Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add valve support #12

Merged
merged 9 commits into from
Oct 30, 2023
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
132 changes: 132 additions & 0 deletions custom_components/alpha_innotec/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 9 additions & 1 deletion custom_components/alpha_innotec/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion custom_components/alpha_innotec/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
1 change: 1 addition & 0 deletions custom_components/alpha_innotec/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

PLATFORMS = [
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.CLIMATE,
]

Expand Down
4 changes: 4 additions & 0 deletions custom_components/alpha_innotec/controller_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/alpha_innotec/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 4 additions & 0 deletions custom_components/alpha_innotec/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions custom_components/alpha_innotec/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%]"
}
}
},
Expand Down
8 changes: 8 additions & 0 deletions custom_components/alpha_innotec/structs/Valve.py
Original file line number Diff line number Diff line change
@@ -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
159 changes: 159 additions & 0 deletions tests/fixtures/controller_api_room_list.json
Original file line number Diff line number Diff line change
@@ -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
}
Loading