diff --git a/homeassistant/components/nasweb/__init__.py b/homeassistant/components/nasweb/__init__.py index 1992cc41c75..43998ef43b3 100644 --- a/homeassistant/components/nasweb/__init__.py +++ b/homeassistant/components/nasweb/__init__.py @@ -19,7 +19,7 @@ from .const import DOMAIN, MANUFACTURER, SUPPORT_EMAIL from .coordinator import NASwebCoordinator from .nasweb_data import NASwebData -PLATFORMS: list[Platform] = [Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] NASWEB_CONFIG_URL = "https://{host}/page" diff --git a/homeassistant/components/nasweb/const.py b/homeassistant/components/nasweb/const.py index ec750c90c8c..9150785d3bb 100644 --- a/homeassistant/components/nasweb/const.py +++ b/homeassistant/components/nasweb/const.py @@ -1,6 +1,7 @@ """Constants for the NASweb integration.""" DOMAIN = "nasweb" +KEY_TEMP_SENSOR = "temp_sensor" MANUFACTURER = "chomtech.pl" STATUS_UPDATE_MAX_TIME_INTERVAL = 60 SUPPORT_EMAIL = "support@chomtech.eu" diff --git a/homeassistant/components/nasweb/coordinator.py b/homeassistant/components/nasweb/coordinator.py index 90dca0f3022..2865bffe9a5 100644 --- a/homeassistant/components/nasweb/coordinator.py +++ b/homeassistant/components/nasweb/coordinator.py @@ -11,16 +11,19 @@ from typing import Any from aiohttp.web import Request, Response from webio_api import WebioAPI -from webio_api.const import KEY_DEVICE_SERIAL, KEY_OUTPUTS, KEY_TYPE, TYPE_STATUS_UPDATE +from webio_api.const import KEY_DEVICE_SERIAL, KEY_TYPE, TYPE_STATUS_UPDATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol -from .const import STATUS_UPDATE_MAX_TIME_INTERVAL +from .const import KEY_TEMP_SENSOR, STATUS_UPDATE_MAX_TIME_INTERVAL _LOGGER = logging.getLogger(__name__) +KEY_INPUTS = "inputs" +KEY_OUTPUTS = "outputs" + class NotificationCoordinator: """Coordinator redirecting push notifications for this integration to appropriate NASwebCoordinator.""" @@ -96,8 +99,11 @@ class NASwebCoordinator(BaseDataUpdateCoordinatorProtocol): self._job = HassJob(self._handle_max_update_interval, job_name) self._unsub_last_update_check: CALLBACK_TYPE | None = None self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} - data: dict[str, Any] = {} - data[KEY_OUTPUTS] = self.webio_api.outputs + data: dict[str, Any] = { + KEY_OUTPUTS: self.webio_api.outputs, + KEY_INPUTS: self.webio_api.inputs, + KEY_TEMP_SENSOR: self.webio_api.temp_sensor, + } self.async_set_updated_data(data) def is_connection_confirmed(self) -> bool: @@ -187,5 +193,9 @@ class NASwebCoordinator(BaseDataUpdateCoordinatorProtocol): async def process_status_update(self, new_status: dict) -> None: """Process status update from NASweb.""" self.webio_api.update_device_status(new_status) - new_data = {KEY_OUTPUTS: self.webio_api.outputs} + new_data = { + KEY_OUTPUTS: self.webio_api.outputs, + KEY_INPUTS: self.webio_api.inputs, + KEY_TEMP_SENSOR: self.webio_api.temp_sensor, + } self.async_set_updated_data(new_data) diff --git a/homeassistant/components/nasweb/icons.json b/homeassistant/components/nasweb/icons.json new file mode 100644 index 00000000000..0055bf2296a --- /dev/null +++ b/homeassistant/components/nasweb/icons.json @@ -0,0 +1,15 @@ +{ + "entity": { + "sensor": { + "sensor_input": { + "default": "mdi:help-circle-outline", + "state": { + "tamper": "mdi:lock-alert", + "active": "mdi:alert", + "normal": "mdi:shield-check-outline", + "problem": "mdi:alert-circle" + } + } + } + } +} diff --git a/homeassistant/components/nasweb/sensor.py b/homeassistant/components/nasweb/sensor.py new file mode 100644 index 00000000000..eb342d7ce92 --- /dev/null +++ b/homeassistant/components/nasweb/sensor.py @@ -0,0 +1,189 @@ +"""Platform for NASweb sensors.""" + +from __future__ import annotations + +import logging +import time + +from webio_api import Input as NASwebInput, TempSensor + +from homeassistant.components.sensor import ( + DOMAIN as DOMAIN_SENSOR, + SensorDeviceClass, + SensorEntity, + SensorStateClass, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + BaseCoordinatorEntity, + BaseDataUpdateCoordinatorProtocol, +) + +from . import NASwebConfigEntry +from .const import DOMAIN, KEY_TEMP_SENSOR, STATUS_UPDATE_MAX_TIME_INTERVAL + +SENSOR_INPUT_TRANSLATION_KEY = "sensor_input" +STATE_UNDEFINED = "undefined" +STATE_TAMPER = "tamper" +STATE_ACTIVE = "active" +STATE_NORMAL = "normal" +STATE_PROBLEM = "problem" + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config: NASwebConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up Sensor platform.""" + coordinator = config.runtime_data + current_inputs: set[int] = set() + + @callback + def _check_entities() -> None: + received_inputs: dict[int, NASwebInput] = { + entry.index: entry for entry in coordinator.webio_api.inputs + } + added = {i for i in received_inputs if i not in current_inputs} + removed = {i for i in current_inputs if i not in received_inputs} + entities_to_add: list[InputStateSensor] = [] + for index in added: + webio_input = received_inputs[index] + if not isinstance(webio_input, NASwebInput): + _LOGGER.error("Cannot create InputStateSensor without NASwebInput") + continue + new_input = InputStateSensor(coordinator, webio_input) + entities_to_add.append(new_input) + current_inputs.add(index) + async_add_entities(entities_to_add) + entity_registry = er.async_get(hass) + for index in removed: + unique_id = f"{DOMAIN}.{config.unique_id}.input.{index}" + if entity_id := entity_registry.async_get_entity_id( + DOMAIN_SENSOR, DOMAIN, unique_id + ): + entity_registry.async_remove(entity_id) + current_inputs.remove(index) + else: + _LOGGER.warning("Failed to remove old input: no entity_id") + + coordinator.async_add_listener(_check_entities) + _check_entities() + + nasweb_temp_sensor = coordinator.data[KEY_TEMP_SENSOR] + temp_sensor = TemperatureSensor(coordinator, nasweb_temp_sensor) + async_add_entities([temp_sensor]) + + +class BaseSensorEntity(SensorEntity, BaseCoordinatorEntity): + """Base class providing common functionality.""" + + def __init__(self, coordinator: BaseDataUpdateCoordinatorProtocol) -> None: + """Initialize base sensor.""" + super().__init__(coordinator) + self._attr_available = False + self._attr_has_entity_name = True + self._attr_should_poll = False + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() + + def _set_attr_available( + self, entity_last_update: float, available: bool | None + ) -> None: + if ( + self.coordinator.last_update is None + or time.time() - entity_last_update >= STATUS_UPDATE_MAX_TIME_INTERVAL + ): + self._attr_available = False + else: + self._attr_available = available if available is not None else False + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + Scheduling updates is not necessary, the coordinator takes care of updates via push notifications. + """ + + +class InputStateSensor(BaseSensorEntity): + """Entity representing NASweb input.""" + + _attr_device_class = SensorDeviceClass.ENUM + _attr_options: list[str] = [ + STATE_UNDEFINED, + STATE_TAMPER, + STATE_ACTIVE, + STATE_NORMAL, + STATE_PROBLEM, + ] + _attr_translation_key = SENSOR_INPUT_TRANSLATION_KEY + + def __init__( + self, + coordinator: BaseDataUpdateCoordinatorProtocol, + nasweb_input: NASwebInput, + ) -> None: + """Initialize InputStateSensor entity.""" + super().__init__(coordinator) + self._input = nasweb_input + self._attr_native_value: str | None = None + self._attr_translation_placeholders = {"index": f"{nasweb_input.index:2d}"} + self._attr_unique_id = ( + f"{DOMAIN}.{self._input.webio_serial}.input.{self._input.index}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._input.webio_serial)}, + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if self._input.state is None or self._input.state in self._attr_options: + self._attr_native_value = self._input.state + else: + _LOGGER.warning("Received unrecognized input state: %s", self._input.state) + self._attr_native_value = None + self._set_attr_available(self._input.last_update, self._input.available) + self.async_write_ha_state() + + +class TemperatureSensor(BaseSensorEntity): + """Entity representing NASweb temperature sensor.""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + + def __init__( + self, + coordinator: BaseDataUpdateCoordinatorProtocol, + nasweb_temp_sensor: TempSensor, + ) -> None: + """Initialize TemperatureSensor entity.""" + super().__init__(coordinator) + self._temp_sensor = nasweb_temp_sensor + self._attr_unique_id = f"{DOMAIN}.{self._temp_sensor.webio_serial}.temp_sensor" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._temp_sensor.webio_serial)} + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self._temp_sensor.value + self._set_attr_available( + self._temp_sensor.last_update, self._temp_sensor.available + ) + self.async_write_ha_state() diff --git a/homeassistant/components/nasweb/strings.json b/homeassistant/components/nasweb/strings.json index 8b93ea10d79..2e1ea55ffcb 100644 --- a/homeassistant/components/nasweb/strings.json +++ b/homeassistant/components/nasweb/strings.json @@ -45,6 +45,18 @@ "switch_output": { "name": "Relay Switch {index}" } + }, + "sensor": { + "sensor_input": { + "name": "Input {index}", + "state": { + "undefined": "Undefined", + "tamper": "Tamper", + "active": "Active", + "normal": "Normal", + "problem": "Problem" + } + } } } }