diff --git a/custom_components/alpha_innotec/base_coordinator.py b/custom_components/alpha_innotec/base_coordinator.py deleted file mode 100644 index f93dc8a..0000000 --- a/custom_components/alpha_innotec/base_coordinator.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Platform for sensor integration.""" -from __future__ import annotations - -import logging - -from homeassistant.core import HomeAssistant - -from . import GatewayAPI -from .const import MODULE_TYPE_SENSOR -from .controller_api import ControllerAPI -from .structs.Thermostat import Thermostat - -_LOGGER = logging.getLogger(__name__) - - -class BaseCoordinator: - - @staticmethod - async def get_thermostats(hass: HomeAssistant, gateway_api: GatewayAPI, controller_api: ControllerAPI) -> list[Thermostat]: - try: - rooms: dict = await hass.async_add_executor_job(gateway_api.all_modules) - - thermostats: list[Thermostat] = [] - - db_modules: dict = await hass.async_add_executor_job(gateway_api.db_modules) - room_list: dict = await hass.async_add_executor_job(controller_api.room_list) - - try: - for room_id in rooms: - room_module = rooms[room_id] - room = await hass.async_add_executor_job(controller_api.room_details, room_id, room_list) - - current_temperature = None - battery_percentage = None - - for module_id in room_module['modules']: - if module_id not in db_modules['modules']: - continue - - module_details = db_modules['modules'][module_id] - - if module_details["type"] == MODULE_TYPE_SENSOR: - current_temperature = module_details["currentTemperature"] - battery_percentage = module_details["battery"] - - thermostat = Thermostat( - identifier=room_id, - name=room['name'], - current_temperature=current_temperature, - desired_temperature=room.get('desiredTemperature'), - minimum_temperature=room.get('minTemperature'), - maximum_temperature=room.get('maxTemperature'), - cooling=room.get('cooling'), - cooling_enabled=room.get('coolingEnabled'), - battery_percentage=battery_percentage - ) - - thermostats.append(thermostat) - except Exception as exception: - _LOGGER.exception("There is an exception: %s", exception) - - return thermostats - except Exception as exception: - raise exception diff --git a/custom_components/alpha_innotec/binary_sensor.py b/custom_components/alpha_innotec/binary_sensor.py index 0900d96..28c23ae 100644 --- a/custom_components/alpha_innotec/binary_sensor.py +++ b/custom_components/alpha_innotec/binary_sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription, \ BinarySensorDeviceClass +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import UndefinedType @@ -15,29 +16,27 @@ ) from .const import DOMAIN, MANUFACTURER +from .coordinator import AlphaInnotecCoordinator from .gateway_api import GatewayAPI from .structs.Valve import Valve _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): """Set up the sensor platform.""" _LOGGER.debug("Setting up binary sensors") - gateway_api = hass.data[DOMAIN][entry.entry_id]['gateway_api'] - - coordinator = AlphaInnotecBinarySensorCoordinator(hass, gateway_api) + coordinator = AlphaInnotecCoordinator(hass) await coordinator.async_config_entry_first_refresh() entities = [] - for valve in coordinator.data: + for valve in coordinator.data['valves']: entities.append(AlphaHomeBinarySensor( coordinator=coordinator, - name=valve.name, description=BinarySensorEntityDescription("", entity_registry_enabled_default=valve.used), valve=valve )) @@ -45,72 +44,14 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class AlphaInnotecBinarySensorCoordinator(DataUpdateCoordinator): - """My custom coordinator.""" - - data: list[Valve] - - def __init__(self, hass: HomeAssistant, gateway_api: GatewayAPI): - """Initialize my coordinator.""" - super().__init__( - hass, - _LOGGER, - name="Alpha Innotec Binary Coordinator", - update_interval=timedelta(seconds=30), - ) - - self.gateway_api: GatewayAPI = gateway_api - - async def _async_update_data(self) -> list[Valve]: - """Fetch data from API endpoint.""" - - db_modules: dict = await self.hass.async_add_executor_job(self.gateway_api.db_modules) - all_modules: dict = await self.hass.async_add_executor_job(self.gateway_api.all_modules) - - valves: list[Valve] = [] - - for module_id in db_modules["modules"]: - module = db_modules["modules"][module_id] - - if module["productId"] != 3: - continue - - for instance in module["instances"]: - valve_id = '0' + instance['instance'] + module['deviceid'][2:] - - used = False - - for room_id in all_modules: - if used is not True: - used = valve_id in all_modules[room_id]["modules"] - - valve = Valve( - identifier=valve_id, - name=module["name"] + '-' + instance['instance'], - instance=instance["instance"], - device_id=module["deviceid"], - device_name=module["name"], - status=instance["status"], - used=used - ) - - valves.append(valve) - - _LOGGER.debug("Finished getting valves from API") - - return valves - - class AlphaHomeBinarySensor(CoordinatorEntity, BinarySensorEntity): """Representation of a Binary Sensor.""" - def __init__(self, coordinator: AlphaInnotecBinarySensorCoordinator, name: str, description: BinarySensorEntityDescription, valve: Valve) -> None: + def __init__(self, coordinator: AlphaInnotecCoordinator, description: BinarySensorEntityDescription, valve: Valve) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator, context=valve.identifier) self.entity_description = description - self._attr_name = name self.valve = valve - self._attr_is_on = valve.status @property def device_info(self) -> DeviceInfo: @@ -125,7 +66,7 @@ def device_info(self) -> DeviceInfo: @property def name(self) -> str | UndefinedType | None: - return self._attr_name + return self.valve.name @property def unique_id(self) -> str: @@ -142,10 +83,11 @@ def device_class(self) -> BinarySensorDeviceClass | None: @callback def _handle_coordinator_update(self) -> None: - for valve in self.coordinator.data: + for valve in self.coordinator.data['valves']: if valve.identifier == self.valve.identifier: self.valve = valve break _LOGGER.debug("Updating binary sensor: %s", self.valve.identifier) + self.async_write_ha_state() diff --git a/custom_components/alpha_innotec/climate.py b/custom_components/alpha_innotec/climate.py index d4ffe4b..dc79a2a 100644 --- a/custom_components/alpha_innotec/climate.py +++ b/custom_components/alpha_innotec/climate.py @@ -6,42 +6,36 @@ from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription, ClimateEntityFeature, HVACAction, \ HVACMode +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, UnitOfTemperature, ) from homeassistant.core import callback, HomeAssistant from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) - -from .base_coordinator import BaseCoordinator +from homeassistant.helpers.typing import UndefinedType +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER from .controller_api import ControllerAPI, Thermostat -from .gateway_api import GatewayAPI +from .coordinator import AlphaInnotecCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): """Set up the sensor platform.""" - controller_api = hass.data[DOMAIN][entry.entry_id]['controller_api'] - gateway_api = hass.data[DOMAIN][entry.entry_id]['gateway_api'] + _LOGGER.debug("Setting up climate sensors") - coordinator = AlphaInnotecClimateCoordinator(hass, controller_api, gateway_api) + coordinator = AlphaInnotecCoordinator(hass) await coordinator.async_config_entry_first_refresh() entities = [] - for thermostat in coordinator.data: + for thermostat in coordinator.data['thermostats']: entities.append(AlphaInnotecClimateSensor( coordinator=coordinator, - api=controller_api, - name=thermostat.name, description=ClimateEntityDescription(""), thermostat=thermostat )) @@ -49,32 +43,6 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class AlphaInnotecClimateCoordinator(DataUpdateCoordinator, BaseCoordinator): - """My custom coordinator.""" - - data: list[Thermostat] - - def __init__(self, hass: HomeAssistant, controller_api: ControllerAPI, gateway_api: GatewayAPI) -> None: - """Initialize my coordinator.""" - super().__init__( - hass, - _LOGGER, - name="Alpha Sensor", - update_interval=timedelta(seconds=30), - ) - - self.controller_api: ControllerAPI = controller_api - self.gateway_api: GatewayAPI = gateway_api - - async def _async_update_data(self) -> list[Thermostat]: - """Fetch data from API endpoint. - - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ - return await self.get_thermostats(self.hass, self.gateway_api, self.controller_api) - - class AlphaInnotecClimateSensor(CoordinatorEntity, ClimateEntity): """Representation of a Sensor.""" @@ -84,15 +52,11 @@ class AlphaInnotecClimateSensor(CoordinatorEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE ) - def __init__(self, coordinator: AlphaInnotecClimateCoordinator, api: ControllerAPI, name: str, description: ClimateEntityDescription, thermostat: Thermostat) -> None: + def __init__(self, coordinator: AlphaInnotecCoordinator, description: ClimateEntityDescription, thermostat: Thermostat) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator, context=thermostat.identifier) - self.api = api self.entity_description = description - self._attr_name = name self.thermostat = thermostat - self._target_temperature = self.thermostat.desired_temperature - self._current_temperature = self.thermostat.current_temperature @property def device_info(self) -> DeviceInfo: @@ -101,7 +65,7 @@ def device_info(self) -> DeviceInfo: identifiers={ (DOMAIN, self.thermostat.identifier) }, - name=self._attr_name, + name=self.thermostat.name, manufacturer=MANUFACTURER, ) @@ -110,27 +74,18 @@ def unique_id(self) -> str: """Return unique ID for this device.""" return self.thermostat.identifier + @property + def name(self) -> str | UndefinedType | None: + return self.thermostat.name + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - - current_thermostat = None - - for thermostat in self.coordinator.data: + for thermostat in self.coordinator.data['thermostats']: if thermostat.identifier == self.thermostat.identifier: - current_thermostat = thermostat + self.thermostat = thermostat - if not current_thermostat: - return - - if current_thermostat == "unknown": - _LOGGER.warning("Current temperature not available for %s", current_thermostat.name) - return - - self._current_temperature = current_thermostat.current_temperature - self._target_temperature = current_thermostat.desired_temperature - - self.thermostat = current_thermostat + _LOGGER.debug("Updating climate sensor: %s", self.thermostat.identifier) self.async_write_ha_state() @@ -138,22 +93,18 @@ def _handle_coordinator_update(self) -> None: @property def current_temperature(self) -> float | None: """Return the current temperature.""" - if self._current_temperature == "unknown": - _LOGGER.warning("Current temperature not available for %s", self.thermostat.name) - return - - return self._current_temperature + return self.thermostat.current_temperature if isinstance(self.thermostat.current_temperature, float) else None @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self._target_temperature + return self.thermostat.desired_temperature if isinstance(self.thermostat.desired_temperature, float) else None async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: - await self.hass.async_add_executor_job(self.api.set_temperature, self.thermostat.identifier, temp) - self._target_temperature = temp + await self.hass.async_add_executor_job(self.coordinator.hass.data[DOMAIN][self.coordinator.config_entry.entry_id]['controller_api'].set_temperature, self.thermostat.identifier, temp) + self.thermostat.desired_temperature = temp @property def hvac_mode(self) -> HVACMode | None: diff --git a/custom_components/alpha_innotec/coordinator.py b/custom_components/alpha_innotec/coordinator.py new file mode 100644 index 0000000..5e4aa95 --- /dev/null +++ b/custom_components/alpha_innotec/coordinator.py @@ -0,0 +1,103 @@ +import logging + +from datetime import timedelta + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import GatewayAPI +from . import ControllerAPI +from .const import MODULE_TYPE_SENSOR, DOMAIN +from .structs.Thermostat import Thermostat +from .structs.Valve import Valve + +_LOGGER = logging.getLogger(__name__) + + +class AlphaInnotecCoordinator(DataUpdateCoordinator): + + data: dict[str, list[Valve | Thermostat]] + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Alpha Innotec", + update_interval=timedelta(seconds=60), + ) + + self.controller_api = hass.data[DOMAIN][self.config_entry.entry_id]['controller_api'] + self.gateway_api = hass.data[DOMAIN][self.config_entry.entry_id]['gateway_api'] + + async def _async_update_data(self) -> dict[str, list[Valve | Thermostat]]: + db_modules: dict = await self.hass.async_add_executor_job(self.gateway_api.db_modules) + all_modules: dict = await self.hass.async_add_executor_job(self.gateway_api.all_modules) + room_list: dict = await self.hass.async_add_executor_job(self.controller_api.room_list) + + thermostats: list[Thermostat] = [] + valves: list[Valve] = [] + + for room_id in all_modules: + room_module = all_modules[room_id] + room = await self.hass.async_add_executor_job(self.controller_api.room_details, room_id, room_list) + + current_temperature = None + battery_percentage = None + + for module_id in room_module['modules']: + if module_id not in db_modules['modules']: + continue + + module_details = db_modules['modules'][module_id] + + if module_details["type"] == MODULE_TYPE_SENSOR: + current_temperature = module_details["currentTemperature"] + battery_percentage = module_details["battery"] + + thermostat = Thermostat( + identifier=room_id, + name=room['name'], + current_temperature=current_temperature, + desired_temperature=room.get('desiredTemperature'), + minimum_temperature=room.get('minTemperature'), + maximum_temperature=room.get('maxTemperature'), + cooling=room.get('cooling'), + cooling_enabled=room.get('coolingEnabled'), + battery_percentage=battery_percentage + ) + + thermostats.append(thermostat) + + for module_id in db_modules["modules"]: + module = db_modules["modules"][module_id] + + if module["productId"] != 3: + continue + + for instance in module["instances"]: + valve_id = '0' + instance['instance'] + module['deviceid'][2:] + + used = False + + for room_id in all_modules: + if used is not True: + used = valve_id in all_modules[room_id]["modules"] + + valve = Valve( + identifier=valve_id, + name=module["name"] + '-' + instance['instance'], + instance=instance["instance"], + device_id=module["deviceid"], + device_name=module["name"], + status=instance["status"], + used=used + ) + + valves.append(valve) + + return { + 'valves': valves, + 'thermostats': thermostats + } diff --git a/custom_components/alpha_innotec/sensor.py b/custom_components/alpha_innotec/sensor.py index 217f888..14d1c76 100644 --- a/custom_components/alpha_innotec/sensor.py +++ b/custom_components/alpha_innotec/sensor.py @@ -2,44 +2,37 @@ from __future__ import annotations import logging -from datetime import timedelta +from datetime import timedelta, date, datetime +from decimal import Decimal from homeassistant.components.sensor import SensorEntity, SensorEntityDescription, SensorDeviceClass +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.typing import StateType, UndefinedType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .base_coordinator import BaseCoordinator from .const import DOMAIN, MANUFACTURER -from .controller_api import ControllerAPI, Thermostat -from .gateway_api import GatewayAPI +from .controller_api import Thermostat +from .coordinator import AlphaInnotecCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): """Set up the sensor platform.""" - controller_api = hass.data[DOMAIN][entry.entry_id]['controller_api'] - gateway_api = hass.data[DOMAIN][entry.entry_id]['gateway_api'] + _LOGGER.debug("Setting up sensors") - coordinator = AlphaInnotecSensorCoordinator(hass, controller_api, gateway_api) + coordinator = AlphaInnotecCoordinator(hass) await coordinator.async_config_entry_first_refresh() entities = [] - for thermostat in coordinator.data: - if thermostat.battery_percentage == "unknown": - _LOGGER.warning("Skipping %s because battery status is unknown.", thermostat.name) - continue - + for thermostat in coordinator.data["thermostats"]: entities.append(AlphaInnotecBatterySensor( coordinator=coordinator, - name=thermostat.name, description=SensorEntityDescription(""), thermostat=thermostat )) @@ -47,45 +40,18 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(entities) -class AlphaInnotecSensorCoordinator(DataUpdateCoordinator, BaseCoordinator): - """My custom coordinator.""" - - data: list[Thermostat] - - def __init__(self, hass: HomeAssistant, controller_api: ControllerAPI, gateway_api: GatewayAPI): - """Initialize my coordinator.""" - super().__init__( - hass, - _LOGGER, - name="Alpha Sensor", - update_interval=timedelta(seconds=30), - ) - - self.controller_api: ControllerAPI = controller_api - self.gateway_api: GatewayAPI = gateway_api - - async def _async_update_data(self) -> list[Thermostat]: - """Fetch data from API endpoint. - - This is the place to pre-process the data to lookup tables - so entities can quickly look up their data. - """ - return await self.get_thermostats(self.hass, self.gateway_api, self.controller_api) - - class AlphaInnotecBatterySensor(CoordinatorEntity, SensorEntity): """Representation of a Sensor.""" _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = "%" - def __init__(self, coordinator: AlphaInnotecSensorCoordinator, name: str, description: SensorEntityDescription, thermostat: Thermostat) -> None: + def __init__(self, coordinator: AlphaInnotecCoordinator, description: SensorEntityDescription, + thermostat: Thermostat) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator, context=thermostat.identifier) self.entity_description = description - self._attr_name = name self.thermostat = thermostat - self._attr_native_value = thermostat.battery_percentage @property def device_info(self) -> DeviceInfo: @@ -94,7 +60,7 @@ def device_info(self) -> DeviceInfo: identifiers={ (DOMAIN, self.thermostat.identifier) }, - name=self._attr_name, + name=self.thermostat.name, manufacturer=MANUFACTURER, ) @@ -103,14 +69,21 @@ def unique_id(self) -> str: """Return unique ID for this device.""" return self.thermostat.identifier + @property + def name(self) -> str | UndefinedType | None: + return self.thermostat.name + + @property + def native_value(self): + return self.thermostat.battery_percentage if self.thermostat.battery_percentage != "unknown" else None + @callback def _handle_coordinator_update(self) -> None: - for thermostat in self.coordinator.data: + for thermostat in self.coordinator.data['thermostats']: if thermostat.identifier == self.thermostat.identifier: self.thermostat = thermostat break - _LOGGER.debug("Updating sensor: %s", self.thermostat.identifier) + _LOGGER.debug("Updating battery sensor: %s", self.thermostat.identifier) - self._attr_native_value = self.thermostat.battery_percentage self.async_write_ha_state()