Compare commits

...
7 changed files with 258 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
*$py.class
.venv/
.pytest_cache/
*.metadata.json
+27
View File
@@ -0,0 +1,27 @@
"""The NestQuest integration."""
from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN, LOGGER
_LOGGER = LOGGER
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the NestQuest integration. YAML configuration is not used, returns True."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up NestQuest from a config entry."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return True
+22
View File
@@ -0,0 +1,22 @@
"""Constants for the NestQuest integration."""
from __future__ import annotations
import logging
DOMAIN = "nestquest"
LOGGER_NAME = "custom_components.nestquest"
LOGGER = logging.getLogger(LOGGER_NAME)
PLATFORMS: list[str] = []
SQLITE_DB_FILENAME = "nestquest.db"
# Configuration keys and default values
CONF_HORIZON_DAYS = "horizon_days"
DEFAULT_HORIZON_DAYS = 14
CONF_DAY_ROLLOVER_TIME = "day_rollover_time"
DEFAULT_DAY_ROLLOVER_TIME = "00:00"
CONF_PANEL_IDLE_TIMEOUT = "panel_idle_timeout"
DEFAULT_PANEL_IDLE_TIMEOUT = 300
+12
View File
@@ -0,0 +1,12 @@
{
"domain": "nestquest",
"name": "NestQuest",
"codeowners": ["@overseer"],
"config_flow": true,
"documentation": "https://code.cubecraftlabs.com/Maniacs_Mansion/NestQuest",
"integration_type": "hub",
"iot_class": "local_polling",
"issue_tracker": "https://code.cubecraftlabs.com/Maniacs_Mansion/NestQuest/issues",
"requirements": [],
"version": "0.1.0"
}
+16
View File
@@ -0,0 +1,16 @@
[project]
name = "nestquest"
version = "0.1.0"
description = "NestQuest Home Assistant Integration"
readme = "README.md"
requires-python = ">=3.11"
dependencies = []
[dependency-groups]
dev = [
"pytest>=8.0.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
+101
View File
@@ -0,0 +1,101 @@
"""Tests for const.py of NestQuest."""
from __future__ import annotations
import ast
from datetime import datetime
import logging
from pathlib import Path
import sys
from unittest.mock import MagicMock
# Mock homeassistant modules if not installed in environment
for _mod in ("homeassistant", "homeassistant.core", "homeassistant.config_entries"):
sys.modules.setdefault(_mod, MagicMock())
from custom_components.nestquest import const
def test_domain() -> None:
"""Verify DOMAIN is defined and equals 'nestquest'."""
assert hasattr(const, "DOMAIN")
assert const.DOMAIN == "nestquest"
assert isinstance(const.DOMAIN, str)
def test_logger() -> None:
"""Verify LOGGER_NAME and LOGGER are defined with correct types and values."""
assert hasattr(const, "LOGGER_NAME")
assert const.LOGGER_NAME == "custom_components.nestquest"
assert isinstance(const.LOGGER_NAME, str)
assert hasattr(const, "LOGGER")
assert isinstance(const.LOGGER, logging.Logger)
assert const.LOGGER.name == "custom_components.nestquest"
def test_platforms() -> None:
"""Verify PLATFORMS is defined as an empty list."""
assert hasattr(const, "PLATFORMS")
assert isinstance(const.PLATFORMS, list)
assert const.PLATFORMS == []
def test_sqlite_db_filename() -> None:
"""Verify SQLITE_DB_FILENAME is defined and equals 'nestquest.db'."""
assert hasattr(const, "SQLITE_DB_FILENAME")
assert const.SQLITE_DB_FILENAME == "nestquest.db"
assert isinstance(const.SQLITE_DB_FILENAME, str)
def test_horizon_days_config() -> None:
"""Verify CONF_HORIZON_DAYS and DEFAULT_HORIZON_DAYS are valid."""
assert hasattr(const, "CONF_HORIZON_DAYS")
assert const.CONF_HORIZON_DAYS == "horizon_days"
assert hasattr(const, "DEFAULT_HORIZON_DAYS")
assert isinstance(const.DEFAULT_HORIZON_DAYS, int)
assert const.DEFAULT_HORIZON_DAYS == 14
assert const.DEFAULT_HORIZON_DAYS > 0
def test_day_rollover_time_config() -> None:
"""Verify CONF_DAY_ROLLOVER_TIME and DEFAULT_DAY_ROLLOVER_TIME are valid."""
assert hasattr(const, "CONF_DAY_ROLLOVER_TIME")
assert const.CONF_DAY_ROLLOVER_TIME == "day_rollover_time"
assert hasattr(const, "DEFAULT_DAY_ROLLOVER_TIME")
assert isinstance(const.DEFAULT_DAY_ROLLOVER_TIME, str)
assert const.DEFAULT_DAY_ROLLOVER_TIME == "00:00"
# Validate time format HH:MM
parsed = datetime.strptime(const.DEFAULT_DAY_ROLLOVER_TIME, "%H:%M")
assert parsed.hour == 0 and parsed.minute == 0
def test_panel_idle_timeout_config() -> None:
"""Verify CONF_PANEL_IDLE_TIMEOUT and DEFAULT_PANEL_IDLE_TIMEOUT are valid."""
assert hasattr(const, "CONF_PANEL_IDLE_TIMEOUT")
assert const.CONF_PANEL_IDLE_TIMEOUT == "panel_idle_timeout"
assert hasattr(const, "DEFAULT_PANEL_IDLE_TIMEOUT")
assert isinstance(const.DEFAULT_PANEL_IDLE_TIMEOUT, int)
assert const.DEFAULT_PANEL_IDLE_TIMEOUT == 300
assert const.DEFAULT_PANEL_IDLE_TIMEOUT > 0
def test_no_hardcoded_domain_or_db_filename_in_modules() -> None:
"""Ensure no module other than const.py hardcodes DOMAIN string or SQLITE_DB_FILENAME."""
pkg_dir = Path(const.__file__).parent
py_files = [f for f in pkg_dir.glob("*.py") if f.name != "const.py"]
assert len(py_files) > 0, "No python modules found in package to check"
for py_file in py_files:
content = py_file.read_text(encoding="utf-8")
tree = ast.parse(content, filename=str(py_file))
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
assert node.value != "nestquest.db", (
f"Found hard-coded database filename '{node.value}' in {py_file.name}:{node.lineno}"
)
if node.value == "nestquest":
raise AssertionError(
f"Found hard-coded domain string '{node.value}' in {py_file.name}:{node.lineno}"
)
+74
View File
@@ -0,0 +1,74 @@
"""Tests for the manifest.json file of NestQuest."""
from __future__ import annotations
import json
from pathlib import Path
import sys
from unittest.mock import MagicMock
# Mock homeassistant modules if not installed in environment
for _mod in ("homeassistant", "homeassistant.core", "homeassistant.config_entries"):
sys.modules.setdefault(_mod, MagicMock())
import pytest
from custom_components.nestquest.const import DOMAIN
MANIFEST_PATH = Path(__file__).parent.parent / "custom_components" / "nestquest" / "manifest.json"
@pytest.fixture
def manifest() -> dict:
"""Load and parse manifest.json."""
assert MANIFEST_PATH.exists(), f"manifest.json does not exist at {MANIFEST_PATH}"
with open(MANIFEST_PATH, encoding="utf-8") as f:
data = json.load(f)
return data
def test_manifest_is_valid_json(manifest: dict) -> None:
"""Verify manifest.json is valid JSON and parsed into a dictionary."""
assert isinstance(manifest, dict)
def test_manifest_domain(manifest: dict) -> None:
"""Verify domain is nestquest."""
assert manifest.get("domain") == "nestquest"
def test_manifest_domain_matches_const(manifest: dict) -> None:
"""Verify domain matches DOMAIN constant from const.py."""
assert manifest.get("domain") == DOMAIN
def test_manifest_version(manifest: dict) -> None:
"""Verify version is a non-empty string."""
assert isinstance(manifest.get("version"), str)
assert len(manifest["version"]) > 0
def test_manifest_config_flow(manifest: dict) -> None:
"""Verify config_flow is True."""
assert manifest.get("config_flow") is True
def test_manifest_iot_class(manifest: dict) -> None:
"""Verify iot_class is local_polling."""
assert manifest.get("iot_class") == "local_polling"
def test_manifest_requirements(manifest: dict) -> None:
"""Verify requirements is an empty list."""
assert manifest.get("requirements") == []
@pytest.mark.parametrize(
"key",
["name", "version", "documentation", "issue_tracker", "codeowners"],
)
def test_manifest_required_keys_exist_and_non_empty(manifest: dict, key: str) -> None:
"""Verify required keys exist and are non-empty."""
assert key in manifest, f"Key '{key}' missing from manifest.json"
val = manifest[key]
assert val is not None, f"Key '{key}' is None"
assert len(val) > 0, f"Key '{key}' is empty"