diff --git a/custom_components/alpha_innotec/binary_sensor.py b/custom_components/alpha_innotec/binary_sensor.py new file mode 100644 index 0000000..47edc20 --- /dev/null +++ b/custom_components/alpha_innotec/binary_sensor.py @@ -0,0 +1,132 @@ +"""Platform for binary sensor integration.""" +from __future__ import annotations + +import logging +from datetime import timedelta + +from homeassistant.components.binary_sensor import BinarySensorEntity, BinarySensorEntityDescription, \ + BinarySensorDeviceClass +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import UndefinedType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, MANUFACTURER +from .gateway_api import GatewayAPI +from .structs.Valve import Valve + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, 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 = AlphaCoordinator(hass, gateway_api) + + await coordinator.async_config_entry_first_refresh() + + entities = [] + + for valve in coordinator.data: + entities.append(AlphaHomeBinarySensor( + coordinator=coordinator, + name=valve.name, + description=BinarySensorEntityDescription(""), + valve=valve + )) + + async_add_entities(entities) + + +class AlphaCoordinator(DataUpdateCoordinator): + """My custom coordinator.""" + + 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. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + + db_modules: dict = await self.hass.async_add_executor_job(self.gateway_api.db_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 = Valve( + identifier=module["deviceid"] + '-' + instance['instance'], + name=module["name"] + '-' + instance['instance'], + instance=instance["instance"], + device_id=module["deviceid"], + device_name=module["name"], + status=instance["status"] + ) + + valves.append(valve) + + _LOGGER.debug("Finished getting valves from API") + + return valves + + +class AlphaHomeBinarySensor(CoordinatorEntity, BinarySensorEntity): + """Representation of a Sensor.""" + + def __init__(self, coordinator: AlphaCoordinator, name: str, 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 + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return DeviceInfo( + identifiers={ + (DOMAIN, self.valve.device_id) + }, + name=self.valve.device_name, + manufacturer=MANUFACTURER, + ) + + @property + def name(self) -> str | UndefinedType | None: + return self._attr_name + + @property + def unique_id(self) -> str: + """Return unique ID for this device.""" + return self.valve.identifier + + @property + def is_on(self) -> bool | None: + return self.valve.status + + @property + def device_class(self) -> BinarySensorDeviceClass | None: + return BinarySensorDeviceClass.OPENING diff --git a/custom_components/alpha_innotec/climate.py b/custom_components/alpha_innotec/climate.py index 3737ce5..3cd0c5d 100644 --- a/custom_components/alpha_innotec/climate.py +++ b/custom_components/alpha_innotec/climate.py @@ -121,6 +121,10 @@ def _handle_coordinator_update(self) -> None: 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 @@ -130,8 +134,12 @@ def _handle_coordinator_update(self) -> None: @property - def current_temperature(self) -> float: + 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 @property diff --git a/custom_components/alpha_innotec/config_flow.py b/custom_components/alpha_innotec/config_flow.py index 26d0bd1..3efd3e6 100644 --- a/custom_components/alpha_innotec/config_flow.py +++ b/custom_components/alpha_innotec/config_flow.py @@ -31,7 +31,7 @@ def validate_input(data: dict) -> dict: return system_information except Exception as exception: - _LOGGER.info("Exception: %s", exception) + _LOGGER.debug("Exception: %s", exception) raise CannotConnect diff --git a/custom_components/alpha_innotec/const.py b/custom_components/alpha_innotec/const.py index 0c7511a..1641551 100644 --- a/custom_components/alpha_innotec/const.py +++ b/custom_components/alpha_innotec/const.py @@ -6,6 +6,7 @@ PLATFORMS = [ Platform.SENSOR, + Platform.BINARY_SENSOR, Platform.CLIMATE, ] diff --git a/custom_components/alpha_innotec/controller_api.py b/custom_components/alpha_innotec/controller_api.py index 023226f..9e274b8 100644 --- a/custom_components/alpha_innotec/controller_api.py +++ b/custom_components/alpha_innotec/controller_api.py @@ -77,7 +77,11 @@ def login(self): "hashed": base64.b64encode(self.encode_signature(self.password, device_token)).decode() }) + if "devicetoken_encrypted" not in response.json(): + raise Exception("Unable to login.") + self.device_token_encrypted = response.json()['devicetoken_encrypted'] + self.user_id = response.json()['userid'] self.device_token_decrypted = self.decrypt2(response.json()['devicetoken_encrypted'], self.password) diff --git a/custom_components/alpha_innotec/manifest.json b/custom_components/alpha_innotec/manifest.json index 47e4cc6..38cdcbe 100644 --- a/custom_components/alpha_innotec/manifest.json +++ b/custom_components/alpha_innotec/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/arjenbos/ha_alpha_innotec/issues", "requirements": ["backports.pbkdf2==0.1", "pycryptodome==3.17"], - "version": "1.0.0" + "version": "1.1.0" } diff --git a/custom_components/alpha_innotec/sensor.py b/custom_components/alpha_innotec/sensor.py index 3e34e02..cb9d392 100644 --- a/custom_components/alpha_innotec/sensor.py +++ b/custom_components/alpha_innotec/sensor.py @@ -33,6 +33,10 @@ async def async_setup_entry(hass, entry, async_add_entities): entities = [] for thermostat in coordinator.data: + if thermostat.battery_percentage == "unknown": + _LOGGER.warning("Skipping %s because battery status is unknown.", thermostat.name) + continue + entities.append(AlphaHomeBatterySensor( coordinator=coordinator, name=thermostat.name, diff --git a/custom_components/alpha_innotec/strings.json b/custom_components/alpha_innotec/strings.json index 7359ec0..cf974e3 100644 --- a/custom_components/alpha_innotec/strings.json +++ b/custom_components/alpha_innotec/strings.json @@ -3,11 +3,13 @@ "flow_title": "{name} ({host})", "step": { "user": { - "description": "Setup Alpha Innotec custom integration.", + "description": "Setup Alpha Innotec.", "data": { + "gateway_ip": "[%key:common::config_flow::data::gateway_ip%]", + "gateway_password": "[%key:common::config_flow::data::gateway_password%]", "controller_ip": "[%key:common::config_flow::data::controller_ip%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "controller_username": "[%key:common::config_flow::data::controller_username%]", + "controller_password": "[%key:common::config_flow::data::controller_password%]" } } }, diff --git a/custom_components/alpha_innotec/structs/Valve.py b/custom_components/alpha_innotec/structs/Valve.py new file mode 100644 index 0000000..5a23c6e --- /dev/null +++ b/custom_components/alpha_innotec/structs/Valve.py @@ -0,0 +1,8 @@ +class Valve: + def __init__(self, identifier: str, name: str, instance: str, device_id: str, device_name: str, status: bool): + self.identifier = identifier + self.name = name + self.instance = instance + self.device_id = device_id + self.device_name = device_name + self.status = status diff --git a/tests/fixtures/controller_api_room_list.json b/tests/fixtures/controller_api_room_list.json new file mode 100644 index 0000000..53388e3 --- /dev/null +++ b/tests/fixtures/controller_api_room_list.json @@ -0,0 +1,159 @@ +{ + "success": true, + "message": "", + "loginRejected": false, + "groups": [ + { + "groupid": 1, + "name": "rooms", + "rooms": [ + { + "id": 1, + "appid": "01010002", + "actualTemperature": 21, + "isComfortMode": false, + "desiredTemperature": 22, + "roomstatus": 51, + "desiredTempDay": 20, + "desiredTempDay2": 18, + "desiredTempNight": 18, + "scheduleTempMin": 15, + "scheduleTempMax": 28, + "minTemperature": 18, + "maxTemperature": 28, + "cooling": false, + "coolingEnabled": true, + "imagepath": "/assets/images/room/default.png", + "name": "Room 1", + "orderindex": 1, + "originalName": "Room 1", + "status": "ok", + "groupid": 1, + "windowPosition": 0 + }, + { + "id": 2, + "appid": "01020002", + "actualTemperature": 20.5, + "isComfortMode": false, + "desiredTemperature": 20, + "roomstatus": 41, + "desiredTempDay": 20, + "desiredTempDay2": 18, + "desiredTempNight": 18, + "scheduleTempMin": 15, + "scheduleTempMax": 28, + "minTemperature": 18, + "maxTemperature": 28, + "cooling": false, + "coolingEnabled": true, + "imagepath": "/assets/images/room/default.png", + "name": "Room 2", + "orderindex": 2, + "originalName": "Room 3", + "status": "ok", + "groupid": 1, + "windowPosition": 0 + }, + { + "id": 3, + "appid": "01030002", + "actualTemperature": 22, + "isComfortMode": false, + "desiredTemperature": 22.5, + "roomstatus": 51, + "desiredTempDay": 20, + "desiredTempDay2": 18, + "desiredTempNight": 18, + "scheduleTempMin": 15, + "scheduleTempMax": 28, + "minTemperature": 18, + "maxTemperature": 28, + "cooling": false, + "coolingEnabled": true, + "imagepath": "/assets/images/room/default.png", + "name": "Room 3", + "orderindex": 3, + "originalName": "Room 3", + "status": "ok", + "groupid": 1, + "windowPosition": 0 + }, + { + "id": 4, + "appid": "01040002", + "isComfortMode": false, + "desiredTemperature": 20, + "roomstatus": 99, + "desiredTempDay": 20, + "desiredTempDay2": 18, + "desiredTempNight": 18, + "scheduleTempMin": 15, + "scheduleTempMax": 28, + "minTemperature": 18, + "maxTemperature": 28, + "cooling": false, + "coolingEnabled": true, + "imagepath": "/assets/images/room/default.png", + "name": "Room 4", + "orderindex": 4, + "originalName": "Room 4", + "status": "problem", + "groupid": 1, + "windowPosition": 0 + }, + { + "id": 5, + "appid": "01050002", + "actualTemperature": 22, + "isComfortMode": false, + "desiredTemperature": 22.5, + "roomstatus": 51, + "desiredTempDay": 20, + "desiredTempDay2": 18, + "desiredTempNight": 18, + "scheduleTempMin": 15, + "scheduleTempMax": 28, + "minTemperature": 18, + "maxTemperature": 28, + "cooling": false, + "coolingEnabled": true, + "imagepath": "/assets/images/room/default.png", + "name": "Room 5", + "orderindex": 5, + "originalName": "Room 5", + "status": "ok", + "groupid": 1, + "windowPosition": 0 + }, + { + "id": 6, + "appid": "01060002", + "actualTemperature": 21.5, + "isComfortMode": false, + "desiredTemperature": 20, + "roomstatus": 41, + "desiredTempDay": 20, + "desiredTempDay2": 18, + "desiredTempNight": 18, + "scheduleTempMin": 15, + "scheduleTempMax": 28, + "minTemperature": 18, + "maxTemperature": 28, + "cooling": false, + "coolingEnabled": true, + "imagepath": "/assets/images/room/default.png", + "name": "Room 6", + "orderindex": 6, + "originalName": "Room 6", + "status": "ok", + "groupid": 1, + "windowPosition": 0 + } + ], + "orderindex": 1 + } + ], + "language": "en", + "performance": 0.907 +} \ No newline at end of file