chore: drop HACS distribution and move CI to Gitea
The GitHub mirror is gone, so the HACS packaging and the GitHub-only CI no longer have anything to run against. Remove hacs.json and the .github/ workflow. Both of its jobs (hassfest and hacs/action) were gated on `github.server_url == 'https://github.com'` and are GitHub-hosted actions, so neither can run on the Gitea runner. Replace them with .gitea/workflows/ci.yml: - lint: ruff check + ruff format --check, version pinned because ruff's default rule set and formatter output move between releases - validate: byte-compile, then scripts/validate_integration.py, which covers the part of hassfest that matters for a manually installed custom component (manifest keys, domain/directory agreement, and translations matching strings.json key for key) README now documents manual installation only, and points at the local CI commands. manifest.json documentation and issue_tracker point at the Gitea repo; NOTICE keeps its upstream attribution. Adopting ruff surfaced two real defects, fixed here: - climate.async_set_hvac_mode raised HomeAssistantError for an unsupported mode from inside a try that catches Exception, so the message was re-wrapped as "Failed to set HVAC mode: Unsupported HVAC mode: ...". The validation is now hoisted above the try. - config_flow.async_step_reauth_confirm swallowed unexpected exceptions into a bare "unknown" error with no log, unlike the user step. It now logs via _LOGGER.exception. Remaining changes are mechanical: import ordering, docstrings on public methods, ClassVar on mutable class attributes, contextlib.suppress, and asyncio.TimeoutError -> TimeoutError (an alias since 3.11). The smoke test now reads the new DknCloudNaClient.socket_connected property rather than reaching into _socket. Co-authored-by: anthropic/claude-opus-5
This commit is contained in:
co-authored by
anthropic/claude-opus-5
parent
d6e935e8d7
commit
706580fab2
@@ -49,6 +49,11 @@ class DknCloudNaClient:
|
||||
token: str | None = None,
|
||||
refresh_token: str | None = None,
|
||||
) -> None:
|
||||
"""Initialise the client.
|
||||
|
||||
Either ``password`` (for a fresh login) or ``token`` /
|
||||
``refresh_token`` (to resume an existing session) should be supplied.
|
||||
"""
|
||||
self._username = username
|
||||
self._session = session
|
||||
self._password = password
|
||||
@@ -190,7 +195,11 @@ class DknCloudNaClient:
|
||||
socketio_path=API_SOCKET_PATH.strip("/"),
|
||||
namespaces=namespaces,
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err: # noqa: BLE001 - socket is best-effort
|
||||
# The Socket.IO connection is an optimisation on top of polling.
|
||||
# python-socketio surfaces transport failures as a grab-bag of
|
||||
# exception types, and none of them should take the integration
|
||||
# down: returning False just means we fall back to the poll cycle.
|
||||
LOGGER.debug("DKN socket connect failed: %s", err)
|
||||
await sio.disconnect()
|
||||
return False
|
||||
@@ -220,6 +229,15 @@ class DknCloudNaClient:
|
||||
async with self._socket_lock:
|
||||
await self._disconnect_socket_locked()
|
||||
|
||||
@property
|
||||
def socket_connected(self) -> bool:
|
||||
"""Return whether the Socket.IO connection is currently up.
|
||||
|
||||
Live updates arrive over this connection; when it is down the
|
||||
integration still works, but only refreshes on the poll interval.
|
||||
"""
|
||||
return self._socket is not None and self._socket.connected
|
||||
|
||||
async def async_send_machine_event(
|
||||
self,
|
||||
installation_id: str,
|
||||
@@ -251,7 +269,7 @@ class DknCloudNaClient:
|
||||
"ack": ack,
|
||||
}
|
||||
LOGGER.debug("DKN socket ack %s %s", namespace, ack)
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err:
|
||||
raise DknConnectionError(str(err) or type(err).__name__) from err
|
||||
|
||||
def pop_last_command_debug(self) -> dict[str, Any] | None:
|
||||
@@ -275,8 +293,11 @@ class DknCloudNaClient:
|
||||
if socket is not None:
|
||||
try:
|
||||
await socket.disconnect()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
except Exception as err: # noqa: BLE001 - teardown must never raise
|
||||
# The socket reference is already dropped, so a failed
|
||||
# disconnect leaves nothing to act on. Log it rather than
|
||||
# swallowing it silently, so a recurring failure is visible.
|
||||
LOGGER.debug("DKN socket disconnect failed: %s", err)
|
||||
|
||||
def _store_tokens(self, data: Any) -> None:
|
||||
"""Persist access and refresh tokens from an API response."""
|
||||
@@ -397,7 +418,7 @@ class DknCloudNaClient:
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
) as response:
|
||||
data = await self._read_response(response)
|
||||
except asyncio.TimeoutError as err:
|
||||
except TimeoutError as err:
|
||||
raise DknConnectionError("Request timed out") from err
|
||||
except ClientError as err:
|
||||
raise DknConnectionError(str(err) or type(err).__name__) from err
|
||||
@@ -451,6 +472,7 @@ class DknCloudNaClient:
|
||||
return f"HTTP {response.status}: {response.reason}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a debug representation with the username and token redacted."""
|
||||
first = self._username[0] if self._username else "?"
|
||||
token_state = "set" if self.token else "none"
|
||||
return f"DknCloudNaClient(u={first}***, token={token_state})"
|
||||
|
||||
@@ -22,6 +22,7 @@ from .entity import DknEntity
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class DknBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Extend BinarySensorEntityDescription with a device_data key."""
|
||||
|
||||
data_key: str = ""
|
||||
|
||||
|
||||
@@ -67,12 +68,14 @@ class DknBinarySensorEntity(DknEntity, BinarySensorEntity):
|
||||
mac: str,
|
||||
description: DknBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialise the binary sensor from its description."""
|
||||
super().__init__(coordinator, mac)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{DOMAIN}_{mac}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the described device flag, or None if absent."""
|
||||
value = self._device_data.get(self.entity_description.data_key)
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -30,6 +30,8 @@ from .const import (
|
||||
SPEED_100,
|
||||
SPEED_AUTO,
|
||||
)
|
||||
from .coordinator import DknCoordinator
|
||||
from .entity import DknEntity
|
||||
from .model import (
|
||||
available_fan_speeds,
|
||||
current_temperature as model_current_temperature,
|
||||
@@ -39,11 +41,9 @@ from .model import (
|
||||
supports_swing,
|
||||
target_temperature as model_target_temperature,
|
||||
target_temperature_key,
|
||||
writable_target_temperature_key,
|
||||
to_device_temperature,
|
||||
writable_target_temperature_key,
|
||||
)
|
||||
from .coordinator import DknCoordinator
|
||||
from .entity import DknEntity
|
||||
|
||||
_MODE_TO_HVAC: dict[int, HVACMode] = {
|
||||
DEVICE_MODE_AUTO: HVACMode.AUTO,
|
||||
@@ -85,7 +85,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
_attr_name = None
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
_attr_hvac_modes = [
|
||||
_attr_hvac_modes: ClassVar[list[HVACMode]] = [
|
||||
HVACMode.OFF,
|
||||
HVACMode.AUTO,
|
||||
HVACMode.COOL,
|
||||
@@ -93,12 +93,13 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
HVACMode.DRY,
|
||||
HVACMode.FAN_ONLY,
|
||||
]
|
||||
_attr_swing_modes = ["off", "swing"]
|
||||
_attr_swing_modes: ClassVar[list[str]] = ["off", "swing"]
|
||||
_attr_min_temp = 16
|
||||
_attr_max_temp = 32
|
||||
_attr_target_temperature_step = 1
|
||||
|
||||
def __init__(self, coordinator: DknCoordinator, mac: str) -> None:
|
||||
"""Initialise the climate entity for a single indoor unit."""
|
||||
super().__init__(coordinator, mac)
|
||||
self._attr_unique_id = f"{DOMAIN}_{mac}"
|
||||
|
||||
@@ -116,11 +117,13 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Return the fan speeds this unit reports as available."""
|
||||
labels = fan_mode_labels(self._device_data)
|
||||
return labels or None
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return the current mode, preferring an unexpired optimistic write."""
|
||||
data = self._device_data
|
||||
power = self._optimistic_get("power", data.get("power", False))
|
||||
if not power:
|
||||
@@ -133,6 +136,11 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return what the unit is inferred to be doing right now.
|
||||
|
||||
The cloud API reports no explicit action, so this is derived from the
|
||||
mode and the gap between room and target temperature.
|
||||
"""
|
||||
mode = self.hvac_mode
|
||||
if mode == HVACMode.OFF:
|
||||
return HVACAction.OFF
|
||||
@@ -157,10 +165,12 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the room temperature in Celsius."""
|
||||
return model_current_temperature(self._device_data)
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the setpoint for the effective mode, in Celsius."""
|
||||
mode = self.hvac_mode
|
||||
if mode in _NO_TARGET_TEMP_MODES:
|
||||
return None
|
||||
@@ -176,23 +186,38 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return None: the unit has a single setpoint, not a range."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return None: the unit has a single setpoint, not a range."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the current fan speed label."""
|
||||
speed = self._device_data.get("speed_state", SPEED_AUTO)
|
||||
return self._optimistic_get("fan_mode", _SPEED_TO_FAN.get(int(speed), "auto"))
|
||||
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Return whether the vertical slats are swinging."""
|
||||
slat = self._device_data.get("slats_vertical_1", 0)
|
||||
return self._optimistic_get("swing_mode", "swing" if int(slat) == 9 else "off")
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Turn the unit off, or switch it on into the requested mode."""
|
||||
# Resolved before the try block below. An unsupported mode is a caller
|
||||
# error, not a cloud failure, and raising it inside the try would let
|
||||
# the blanket handler re-wrap it as "Failed to set HVAC mode:
|
||||
# Unsupported HVAC mode: ...".
|
||||
mode: int | None = None
|
||||
if hvac_mode != HVACMode.OFF:
|
||||
mode = _HVAC_TO_MODE.get(hvac_mode)
|
||||
if mode is None:
|
||||
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||
|
||||
installation_id = self._installation_id
|
||||
async with self._get_device_lock():
|
||||
try:
|
||||
@@ -210,9 +235,6 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
device_value=False,
|
||||
)
|
||||
else:
|
||||
mode = _HVAC_TO_MODE.get(hvac_mode)
|
||||
if mode is None:
|
||||
raise HomeAssistantError(f"Unsupported HVAC mode: {hvac_mode}")
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._command_mac, "power", True
|
||||
)
|
||||
@@ -228,13 +250,14 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
device_key="mode",
|
||||
device_value=mode,
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err:
|
||||
raise HomeAssistantError(f"Failed to set HVAC mode: {err}") from err
|
||||
|
||||
self._schedule_refresh()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the target temperature, optionally changing mode first."""
|
||||
hvac_mode = kwargs.get("hvac_mode")
|
||||
if hvac_mode is not None:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
@@ -269,7 +292,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._command_mac, property_name, device_temp
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err:
|
||||
raise HomeAssistantError(f"Failed to set temperature: {err}") from err
|
||||
|
||||
self._optimistic_set(
|
||||
@@ -282,6 +305,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set the fan speed."""
|
||||
speed = _FAN_TO_SPEED.get(fan_mode)
|
||||
if speed is None:
|
||||
raise HomeAssistantError(f"Unsupported fan mode: {fan_mode}")
|
||||
@@ -294,7 +318,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._command_mac, "speed_state", speed
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err:
|
||||
raise HomeAssistantError(f"Failed to set fan mode: {err}") from err
|
||||
|
||||
self._optimistic_set(
|
||||
@@ -304,6 +328,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Start or stop the vertical slat swing."""
|
||||
if swing_mode not in {"off", "swing"}:
|
||||
raise HomeAssistantError(f"Unsupported swing mode: {swing_mode}")
|
||||
if not supports_swing(self._device_data):
|
||||
@@ -316,7 +341,7 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
await self.coordinator.client.async_send_machine_event(
|
||||
installation_id, self._command_mac, "slats_vertical_1", slat
|
||||
)
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err:
|
||||
raise HomeAssistantError(f"Failed to set swing mode: {err}") from err
|
||||
|
||||
self._optimistic_set(
|
||||
@@ -326,9 +351,11 @@ class DknClimateEntity(DknEntity, ClimateEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the unit on into auto mode."""
|
||||
await self.async_set_hvac_mode(HVACMode.AUTO)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the unit off."""
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,11 +6,11 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import voluptuous as vol
|
||||
|
||||
from .api import DknAuthError, DknCloudNaClient, DknConnectionError
|
||||
from .const import (
|
||||
@@ -49,6 +49,7 @@ class DknConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialise the flow's accumulated state."""
|
||||
self._email: str = ""
|
||||
self._scan_interval: int = DEFAULT_SCAN_INTERVAL
|
||||
self._expose_pii: bool = False
|
||||
@@ -59,6 +60,7 @@ class DknConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
def async_get_options_flow(
|
||||
entry: config_entries.ConfigEntry,
|
||||
) -> config_entries.OptionsFlow:
|
||||
"""Return the options flow for an existing entry."""
|
||||
return DknOptionsFlow(entry)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -68,6 +70,7 @@ class DknConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> config_entries.FlowResult:
|
||||
"""Collect credentials and verify them against the cloud."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
@@ -91,7 +94,7 @@ class DknConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except DknConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error during login")
|
||||
errors["base"] = "unknown"
|
||||
finally:
|
||||
@@ -158,14 +161,17 @@ class DknConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: dict[str, Any]
|
||||
self,
|
||||
entry_data: dict[str, Any], # noqa: ARG002 - required by the HA flow API
|
||||
) -> config_entries.FlowResult:
|
||||
"""Handle re-authentication after the refresh token stops working."""
|
||||
self._reauth_entry_id = (self.context or {}).get("entry_id")
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> config_entries.FlowResult:
|
||||
"""Prompt for the password again and store a fresh token."""
|
||||
entry = None
|
||||
if getattr(self, "_reauth_entry_id", None):
|
||||
entry = self.hass.config_entries.async_get_entry(self._reauth_entry_id)
|
||||
@@ -192,7 +198,8 @@ class DknConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
except DknConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error during reauthentication")
|
||||
errors["base"] = "unknown"
|
||||
finally:
|
||||
client.clear_password()
|
||||
@@ -216,11 +223,13 @@ class DknOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Options flow: scan interval + PII toggle."""
|
||||
|
||||
def __init__(self, entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialise the options flow for an existing entry."""
|
||||
self._entry = entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> config_entries.FlowResult:
|
||||
"""Show and persist the scan interval and PII toggle."""
|
||||
opts = self._entry.options
|
||||
defaults = {
|
||||
CONF_SCAN_INTERVAL: int(
|
||||
|
||||
@@ -31,8 +31,9 @@ SOCKET_RECONNECT_ATTEMPTS = 5
|
||||
# Config/options keys
|
||||
CONF_SCAN_INTERVAL = "scan_interval"
|
||||
CONF_EXPOSE_PII = "expose_pii"
|
||||
CONF_USER_TOKEN = "user_token"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
# These two are config-entry key names, not credentials.
|
||||
CONF_USER_TOKEN = "user_token" # noqa: S105
|
||||
CONF_REFRESH_TOKEN = "refresh_token" # noqa: S105
|
||||
|
||||
# Defaults
|
||||
DEFAULT_SCAN_INTERVAL = 60 # seconds
|
||||
|
||||
@@ -12,7 +12,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import DknAuthError, DknCloudNaClient, DknConnectionError
|
||||
from .const import CONF_REFRESH_TOKEN, CONF_USER_TOKEN, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
|
||||
from .const import (
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONF_USER_TOKEN,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
||||
|
||||
class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
@@ -34,6 +40,7 @@ class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
entry: ConfigEntry,
|
||||
client: DknCloudNaClient,
|
||||
) -> None:
|
||||
"""Initialise the coordinator with the entry's scan interval."""
|
||||
scan_interval = int(entry.options.get("scan_interval", DEFAULT_SCAN_INTERVAL))
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -66,7 +73,7 @@ class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
raise UpdateFailed(f"Cannot reach DKN Cloud NA: {err}") from err
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Unexpected error: {type(err).__name__}") from err
|
||||
|
||||
self._persist_tokens_if_changed()
|
||||
|
||||
@@ -34,6 +34,7 @@ class DknEntity(CoordinatorEntity[DknCoordinator]):
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: DknCoordinator, mac: str) -> None:
|
||||
"""Initialise the entity for the device with this MAC."""
|
||||
super().__init__(coordinator)
|
||||
self._mac = mac
|
||||
|
||||
@@ -52,6 +53,7 @@ class DknEntity(CoordinatorEntity[DknCoordinator]):
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device registry entry for this indoor unit."""
|
||||
data = self._device_data
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._mac)},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "dkncloudna",
|
||||
"name": "DKN Cloud NA",
|
||||
"codeowners": ["@lavoiesl"],
|
||||
"codeowners": ["@thatguygriff"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/lavoiesl/homeassistant-dkncloudna",
|
||||
"documentation": "https://git.unsupervised.ca/GitHub/homeassistant-dkncloudna",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/lavoiesl/homeassistant-dkncloudna/issues",
|
||||
"issue_tracker": "https://git.unsupervised.ca/GitHub/homeassistant-dkncloudna/issues",
|
||||
"requirements": ["python-socketio>=4.6.1,<5"],
|
||||
"version": "0.2.1"
|
||||
}
|
||||
|
||||
@@ -91,12 +91,14 @@ class DknSensorEntity(DknEntity, SensorEntity):
|
||||
mac: str,
|
||||
description: DknSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialise the sensor from its description."""
|
||||
super().__init__(coordinator, mac)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{DOMAIN}_{mac}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> Any:
|
||||
"""Return the described device value, converted where needed."""
|
||||
if self.entity_description.data_key == "work_temp":
|
||||
return current_temperature(self._device_data)
|
||||
if self.entity_description.data_key == "ext_temp":
|
||||
|
||||
Reference in New Issue
Block a user