Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
labels:
- dependencies
- github-actions
2 changes: 1 addition & 1 deletion .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Create Release
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Set manifest version number
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
with:
timezoneLinux: "Europe/Berlin"
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ jobs:
runs-on: "ubuntu-latest"
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Hassfest validation
uses: home-assistant/actions/hassfest@master
validate-hacs:
name: Validate HACS
runs-on: "ubuntu-latest"
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: HACS validation
uses: hacs/action@main
with:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ devenv.local.nix

# pre-commit
.pre-commit-config.yaml

### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
8 changes: 3 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
---
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
rev: v0.13.2
hooks:
# Run the linter.
- id: ruff
# Run the linter with auto-fix.
- id: ruff-check
args: [--fix]
pass_filenames: false
# Run the formatter.
- id: ruff-format
- repo: https://github.com/adrienverge/yamllint.git
Expand Down
44 changes: 39 additions & 5 deletions custom_components/tech/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,45 @@
from . import assets
from .const import DOMAIN, PLATFORMS, USER_ID
from .coordinator import TechCoordinator
from .tech import Tech

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # pylint: disable=unused-argument
"""Set up the Tech Controllers component."""
"""Set up the Tech Controllers integration via YAML configuration.

This entry point exists for completeness; the integration relies on
config entries, so the function simply returns ``True`` to signal
successful initialization.

Args:
hass: Home Assistant instance.
config: Top-level configuration data (unused).

Returns:
``True`` to indicate setup should continue.

"""
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Tech Controllers from a config entry."""
"""Initialize Tech Controllers from a config entry.

The method creates a :class:`TechCoordinator`, refreshes the initial
dataset, loads translated subtitles, and forwards the setup to the
supported platforms.

Args:
hass: Home Assistant instance.
entry: Active configuration entry for the integration.

Returns:
``True`` if the entry was set up successfully.

"""
_LOGGER.debug("Setting up component's entry")
_LOGGER.debug("Entry id: %s", str(entry.entry_id))
_LOGGER.debug(
Expand All @@ -47,15 +72,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

await coordinator.async_config_entry_first_refresh()

await assets.load_subtitles(language_code, Tech(websession, user_id, token))
await assets.load_subtitles(language_code, coordinator.api)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
"""Unload a config entry.

Args:
hass: Home Assistant instance.
entry: Configuration entry to unload.

Returns:
``True`` if all platforms were unloaded successfully.

"""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
Expand Down
70 changes: 38 additions & 32 deletions custom_components/tech/assets.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,83 @@
"""Assets for translations."""
"""Helper utilities for working with integration assets."""

from __future__ import annotations

from collections.abc import Iterable
import logging
from typing import Any

from .const import DEFAULT_ICON, ICON_BY_ID, ICON_BY_TYPE, TXT_ID_BY_TYPE

logging.basicConfig(level=logging.DEBUG)
_LOGGER = logging.getLogger(__name__)

TRANSLATIONS = None
_REDACTED_VALUE = "***HIDDEN***"

TranslationsType = dict[str, Any]


TRANSLATIONS: TranslationsType | None = None

def redact(entry_data: dict, keys: list) -> str:
"""Return a copy of entry_data with the specified fields redacted.

def redact(entry_data: dict[str, Any], keys: Iterable[str]) -> str:
"""Return a string representation of ``entry_data`` with selected keys masked.

Args:
entry_data (dict): The data to redact.
keys (list): The list of keys to redact.
entry_data: Source mapping that may contain sensitive values.
keys: Sequence of keys whose values should be replaced.

Returns:
str: The redacted data.
Stringified version of ``entry_data`` with sensitive values replaced by
``***HIDDEN***``.

"""
sanitized_data = entry_data.copy()
for key in keys:
if key in sanitized_data:
sanitized_data[key] = "***HIDDEN***"
sanitized_data[key] = _REDACTED_VALUE
return str(sanitized_data)


async def load_subtitles(language: str, api):
"""Load subtitles for the specified language.
async def load_subtitles(language: str, api) -> None:
"""Load translated subtitles for the active integration language.

Args:
language (str): The language code for the subtitles. Defaults to "pl".
api: object to use Tech api

Returns:
None
language: Home Assistant language code to retrieve from the API.
api: Authenticated Tech API client exposing ``get_translations``.

"""
global TRANSLATIONS # noqa: PLW0603 # pylint: disable=global-statement
TRANSLATIONS = await api.get_translations(language)


def get_text(text_id) -> str:
"""Get text by id."""
def get_text(text_id: int) -> str:
"""Return the translated string for a subtitle identifier."""
if TRANSLATIONS is not None and text_id != 0:
return TRANSLATIONS["data"].get(str(text_id), f"txtId {text_id}")
return TRANSLATIONS.get("data", {}).get(str(text_id), f"txtId {text_id}")
return f"txtId {text_id}"


def get_id_from_text(text) -> int:
"""Get id from text (reverse lookup needed for migration)."""
if text != "":
_LOGGER.debug("👰 text to lookup: %s", text)
def get_id_from_text(text: str) -> int:
"""Return the translation identifier for a given text value."""
if text:
_LOGGER.debug("Looking up translation id for text: %s", text)
if TRANSLATIONS is not None:
return int(
[key for key, value in TRANSLATIONS["data"].items() if value == text][0]
)
for key, value in TRANSLATIONS.get("data", {}).items():
if value == text:
return int(key)
return 0


def get_text_by_type(text_type) -> str:
"""Get text by type."""
def get_text_by_type(text_type: int) -> str:
"""Return the translated label associated with a tile type."""
text_id = TXT_ID_BY_TYPE.get(text_type, f"type {text_type}")
return get_text(text_id)


def get_icon(icon_id) -> str:
"""Get icon by id."""
def get_icon(icon_id: int) -> str:
"""Return the Material Design icon name mapped to ``icon_id``."""
return ICON_BY_ID.get(icon_id, DEFAULT_ICON)


def get_icon_by_type(icon_type) -> str:
"""Get icon by type."""
def get_icon_by_type(icon_type: int) -> str:
"""Return the default icon assigned to the provided tile type."""
return ICON_BY_TYPE.get(icon_type, DEFAULT_ICON)
25 changes: 20 additions & 5 deletions custom_components/tech/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ async def async_setup_entry(
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entry."""
"""Set up Tech binary sensors for a newly created config entry.

Args:
hass: Home Assistant instance.
config_entry: Integration entry containing controller metadata.
async_add_entities: Callback used to register entities with Home Assistant.

"""
_LOGGER.debug("Setting up entry for sensors…")
controller = config_entry.data[CONTROLLER]
coordinator = hass.data[DOMAIN][config_entry.entry_id]
Expand Down Expand Up @@ -67,12 +74,12 @@ async def async_setup_entry(


class TileBinarySensor(TileEntity, binary_sensor.BinarySensorEntity):
"""Representation of a TileBinarySensor."""
"""Base class for Tech tiles that expose binary sensor semantics."""

_attr_entity_category = EntityCategory.DIAGNOSTIC

def get_state(self, device):
"""Get the state of the device."""
"""Return the raw binary state extracted from ``device`` data."""

@property
def unique_id(self) -> str:
Expand All @@ -96,7 +103,15 @@ class RelaySensor(TileBinarySensor):
def __init__(
self, device, coordinator: TechCoordinator, config_entry, device_class=None
) -> None:
"""Initialize the tile relay sensor."""
"""Initialize the relay-backed binary sensor tile.

Args:
device: Tile payload returned from the Tech API.
coordinator: Shared Tech data coordinator instance.
config_entry: Config entry providing controller metadata.
device_class: Optional Home Assistant device class for the sensor.

"""
TileBinarySensor.__init__(self, device, coordinator, config_entry)
self._attr_device_class = device_class
self._coordinator = coordinator
Expand All @@ -107,5 +122,5 @@ def __init__(
self._attr_icon = assets.get_icon_by_type(device[CONF_TYPE])

def get_state(self, device):
"""Get device state."""
"""Return the on/off working status for the provided ``device`` payload."""
return device[CONF_PARAMS]["workingStatus"]
Loading
Loading