Improve data handling for Sensibo (#68419)
This commit is contained in:
parent
5fffe9b22f
commit
d23d19f9e6
10 changed files with 81 additions and 287 deletions
|
@ -3,7 +3,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
|
||||||
|
from pysensibo.model import MotionSensor, SensiboDevice
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
|
@ -15,8 +16,8 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN
|
||||||
from .coordinator import MotionSensor, SensiboDataUpdateCoordinator
|
from .coordinator import SensiboDataUpdateCoordinator
|
||||||
from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity
|
from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ class MotionBaseEntityDescriptionMixin:
|
||||||
class DeviceBaseEntityDescriptionMixin:
|
class DeviceBaseEntityDescriptionMixin:
|
||||||
"""Mixin for required Sensibo base description keys."""
|
"""Mixin for required Sensibo base description keys."""
|
||||||
|
|
||||||
value_fn: Callable[[dict[str, Any]], bool | None]
|
value_fn: Callable[[SensiboDevice], bool | None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -79,7 +80,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = (
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
name="Room Occupied",
|
name="Room Occupied",
|
||||||
icon="mdi:motion-sensor",
|
icon="mdi:motion-sensor",
|
||||||
value_fn=lambda data: data["room_occupied"],
|
value_fn=lambda data: data.room_occupied,
|
||||||
),
|
),
|
||||||
SensiboDeviceBinarySensorEntityDescription(
|
SensiboDeviceBinarySensorEntityDescription(
|
||||||
key="update_available",
|
key="update_available",
|
||||||
|
@ -87,7 +88,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = (
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
name="Update Available",
|
name="Update Available",
|
||||||
icon="mdi:rocket-launch",
|
icon="mdi:rocket-launch",
|
||||||
value_fn=lambda data: data["update_available"],
|
value_fn=lambda data: data.update_available,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -100,22 +101,19 @@ async def async_setup_entry(
|
||||||
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
entities: list[SensiboMotionSensor | SensiboDeviceSensor] = []
|
entities: list[SensiboMotionSensor | SensiboDeviceSensor] = []
|
||||||
LOGGER.debug("parsed data: %s", coordinator.data.parsed)
|
|
||||||
entities.extend(
|
entities.extend(
|
||||||
SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description)
|
SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description)
|
||||||
for device_id, device_data in coordinator.data.parsed.items()
|
for device_id, device_data in coordinator.data.parsed.items()
|
||||||
for sensor_id, sensor_data in device_data["motion_sensors"].items()
|
for sensor_id, sensor_data in device_data.motion_sensors.items()
|
||||||
for description in MOTION_SENSOR_TYPES
|
for description in MOTION_SENSOR_TYPES
|
||||||
if device_data["motion_sensors"]
|
if device_data.motion_sensors
|
||||||
)
|
)
|
||||||
LOGGER.debug("start device %s", entities)
|
|
||||||
entities.extend(
|
entities.extend(
|
||||||
SensiboDeviceSensor(coordinator, device_id, description)
|
SensiboDeviceSensor(coordinator, device_id, description)
|
||||||
for description in DEVICE_SENSOR_TYPES
|
for description in DEVICE_SENSOR_TYPES
|
||||||
for device_id, device_data in coordinator.data.parsed.items()
|
for device_id, device_data in coordinator.data.parsed.items()
|
||||||
if device_data[description.key] is not None
|
if getattr(device_data, description.key) is not None
|
||||||
)
|
)
|
||||||
LOGGER.debug("list: %s", entities)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
@ -144,7 +142,7 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity):
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{sensor_id}-{entity_description.key}"
|
self._attr_unique_id = f"{sensor_id}-{entity_description.key}"
|
||||||
self._attr_name = (
|
self._attr_name = (
|
||||||
f"{self.device_data['name']} Motion Sensor {entity_description.name}"
|
f"{self.device_data.name} Motion Sensor {entity_description.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -171,7 +169,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity):
|
||||||
)
|
)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
||||||
self._attr_name = f"{self.device_data['name']} {entity_description.name}"
|
self._attr_name = f"{self.device_data.name} {entity_description.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool | None:
|
def is_on(self) -> bool | None:
|
||||||
|
|
|
@ -126,11 +126,9 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
"""Initiate SensiboClimate."""
|
"""Initiate SensiboClimate."""
|
||||||
super().__init__(coordinator, device_id)
|
super().__init__(coordinator, device_id)
|
||||||
self._attr_unique_id = device_id
|
self._attr_unique_id = device_id
|
||||||
self._attr_name = coordinator.data.parsed[device_id]["name"]
|
self._attr_name = self.device_data.name
|
||||||
self._attr_temperature_unit = (
|
self._attr_temperature_unit = (
|
||||||
TEMP_CELSIUS
|
TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT
|
||||||
if coordinator.data.parsed[device_id]["temp_unit"] == "C"
|
|
||||||
else TEMP_FAHRENHEIT
|
|
||||||
)
|
)
|
||||||
self._attr_supported_features = self.get_features()
|
self._attr_supported_features = self.get_features()
|
||||||
self._attr_precision = PRECISION_TENTHS
|
self._attr_precision = PRECISION_TENTHS
|
||||||
|
@ -138,7 +136,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
def get_features(self) -> int:
|
def get_features(self) -> int:
|
||||||
"""Get supported features."""
|
"""Get supported features."""
|
||||||
features = 0
|
features = 0
|
||||||
for key in self.coordinator.data.parsed[self.unique_id]["full_features"]:
|
for key in self.device_data.full_features:
|
||||||
if key in FIELD_TO_FLAG:
|
if key in FIELD_TO_FLAG:
|
||||||
features |= FIELD_TO_FLAG[key]
|
features |= FIELD_TO_FLAG[key]
|
||||||
return features
|
return features
|
||||||
|
@ -146,30 +144,27 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def current_humidity(self) -> int | None:
|
def current_humidity(self) -> int | None:
|
||||||
"""Return the current humidity."""
|
"""Return the current humidity."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["humidity"]
|
return self.device_data.humidity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> str:
|
def hvac_mode(self) -> str:
|
||||||
"""Return hvac operation."""
|
"""Return hvac operation."""
|
||||||
return (
|
return (
|
||||||
SENSIBO_TO_HA[self.coordinator.data.parsed[self.unique_id]["hvac_mode"]]
|
SENSIBO_TO_HA[self.device_data.hvac_mode]
|
||||||
if self.coordinator.data.parsed[self.unique_id]["on"]
|
if self.device_data.device_on
|
||||||
else HVAC_MODE_OFF
|
else HVAC_MODE_OFF
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_modes(self) -> list[str]:
|
def hvac_modes(self) -> list[str]:
|
||||||
"""Return the list of available hvac operation modes."""
|
"""Return the list of available hvac operation modes."""
|
||||||
return [
|
return [SENSIBO_TO_HA[mode] for mode in self.device_data.hvac_modes]
|
||||||
SENSIBO_TO_HA[mode]
|
|
||||||
for mode in self.coordinator.data.parsed[self.unique_id]["hvac_modes"]
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return convert_temperature(
|
return convert_temperature(
|
||||||
self.coordinator.data.parsed[self.unique_id]["temp"],
|
self.device_data.temp,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
self.temperature_unit,
|
self.temperature_unit,
|
||||||
)
|
)
|
||||||
|
@ -177,57 +172,51 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["target_temp"]
|
return self.device_data.target_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_step(self) -> float | None:
|
def target_temperature_step(self) -> float | None:
|
||||||
"""Return the supported step of target temperature."""
|
"""Return the supported step of target temperature."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["temp_step"]
|
return self.device_data.temp_step
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return the fan setting."""
|
"""Return the fan setting."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["fan_mode"]
|
return self.device_data.fan_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_modes(self) -> list[str] | None:
|
def fan_modes(self) -> list[str] | None:
|
||||||
"""Return the list of available fan modes."""
|
"""Return the list of available fan modes."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["fan_modes"]
|
return self.device_data.fan_modes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swing_mode(self) -> str | None:
|
def swing_mode(self) -> str | None:
|
||||||
"""Return the swing setting."""
|
"""Return the swing setting."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["swing_mode"]
|
return self.device_data.swing_mode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swing_modes(self) -> list[str] | None:
|
def swing_modes(self) -> list[str] | None:
|
||||||
"""Return the list of available swing modes."""
|
"""Return the list of available swing modes."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["swing_modes"]
|
return self.device_data.swing_modes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_temp(self) -> float:
|
def min_temp(self) -> float:
|
||||||
"""Return the minimum temperature."""
|
"""Return the minimum temperature."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["temp_list"][0]
|
return self.device_data.temp_list[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_temp(self) -> float:
|
def max_temp(self) -> float:
|
||||||
"""Return the maximum temperature."""
|
"""Return the maximum temperature."""
|
||||||
return self.coordinator.data.parsed[self.unique_id]["temp_list"][-1]
|
return self.device_data.temp_list[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return (
|
return self.device_data.available and super().available
|
||||||
self.coordinator.data.parsed[self.unique_id]["available"]
|
|
||||||
and super().available
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs) -> None:
|
async def async_set_temperature(self, **kwargs) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
if (
|
if "targetTemperature" not in self.device_data.active_features:
|
||||||
"targetTemperature"
|
|
||||||
not in self.coordinator.data.parsed[self.unique_id]["active_features"]
|
|
||||||
):
|
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Current mode doesn't support setting Target Temperature"
|
"Current mode doesn't support setting Target Temperature"
|
||||||
)
|
)
|
||||||
|
@ -238,23 +227,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
if temperature == self.target_temperature:
|
if temperature == self.target_temperature:
|
||||||
return
|
return
|
||||||
|
|
||||||
if temperature not in self.coordinator.data.parsed[self.unique_id]["temp_list"]:
|
if temperature not in self.device_data.temp_list:
|
||||||
# Requested temperature is not supported.
|
# Requested temperature is not supported.
|
||||||
if (
|
if temperature > self.device_data.temp_list[-1]:
|
||||||
temperature
|
temperature = self.device_data.temp_list[-1]
|
||||||
> self.coordinator.data.parsed[self.unique_id]["temp_list"][-1]
|
|
||||||
):
|
|
||||||
temperature = self.coordinator.data.parsed[self.unique_id]["temp_list"][
|
|
||||||
-1
|
|
||||||
]
|
|
||||||
|
|
||||||
elif (
|
elif temperature < self.device_data.temp_list[0]:
|
||||||
temperature
|
temperature = self.device_data.temp_list[0]
|
||||||
< self.coordinator.data.parsed[self.unique_id]["temp_list"][0]
|
|
||||||
):
|
|
||||||
temperature = self.coordinator.data.parsed[self.unique_id]["temp_list"][
|
|
||||||
0
|
|
||||||
]
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
@ -263,10 +242,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||||
"""Set new target fan mode."""
|
"""Set new target fan mode."""
|
||||||
if (
|
if "fanLevel" not in self.device_data.active_features:
|
||||||
"fanLevel"
|
|
||||||
not in self.coordinator.data.parsed[self.unique_id]["active_features"]
|
|
||||||
):
|
|
||||||
raise HomeAssistantError("Current mode doesn't support setting Fanlevel")
|
raise HomeAssistantError("Current mode doesn't support setting Fanlevel")
|
||||||
|
|
||||||
await self._async_set_ac_state_property("fanLevel", fan_mode)
|
await self._async_set_ac_state_property("fanLevel", fan_mode)
|
||||||
|
@ -278,7 +254,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Turn on if not currently on.
|
# Turn on if not currently on.
|
||||||
if not self.coordinator.data.parsed[self.unique_id]["on"]:
|
if not self.device_data.device_on:
|
||||||
await self._async_set_ac_state_property("on", True)
|
await self._async_set_ac_state_property("on", True)
|
||||||
|
|
||||||
await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode])
|
await self._async_set_ac_state_property("mode", HA_TO_SENSIBO[hvac_mode])
|
||||||
|
@ -286,10 +262,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
|
|
||||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||||
"""Set new target swing operation."""
|
"""Set new target swing operation."""
|
||||||
if (
|
if "swing" not in self.device_data.active_features:
|
||||||
"swing"
|
|
||||||
not in self.coordinator.data.parsed[self.unique_id]["active_features"]
|
|
||||||
):
|
|
||||||
raise HomeAssistantError("Current mode doesn't support setting Swing")
|
raise HomeAssistantError("Current mode doesn't support setting Swing")
|
||||||
|
|
||||||
await self._async_set_ac_state_property("swing", swing_mode)
|
await self._async_set_ac_state_property("swing", swing_mode)
|
||||||
|
@ -309,13 +282,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity):
|
||||||
params = {
|
params = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"value": value,
|
"value": value,
|
||||||
"ac_states": self.coordinator.data.parsed[self.unique_id]["ac_states"],
|
"ac_states": self.device_data.ac_states,
|
||||||
"assumed_state": assumed_state,
|
"assumed_state": assumed_state,
|
||||||
}
|
}
|
||||||
result = await self.async_send_command("set_ac_state", params)
|
result = await self.async_send_command("set_ac_state", params)
|
||||||
|
|
||||||
if result["result"]["status"] == "Success":
|
if result["result"]["status"] == "Success":
|
||||||
self.coordinator.data.parsed[self.unique_id][AC_STATE_TO_DATA[name]] = value
|
setattr(self.device_data, AC_STATE_TO_DATA[name], value)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
"""DataUpdateCoordinator for the Sensibo integration."""
|
"""DataUpdateCoordinator for the Sensibo integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pysensibo import SensiboClient
|
from pysensibo import SensiboClient
|
||||||
from pysensibo.exceptions import AuthenticationError, SensiboError
|
from pysensibo.exceptions import AuthenticationError, SensiboError
|
||||||
|
from pysensibo.model import SensiboData
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
@ -17,33 +16,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||||
|
|
||||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
|
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, TIMEOUT
|
||||||
|
|
||||||
MAX_POSSIBLE_STEP = 1000
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MotionSensor:
|
|
||||||
"""Dataclass for motionsensors."""
|
|
||||||
|
|
||||||
id: str
|
|
||||||
alive: bool | None = None
|
|
||||||
motion: bool | None = None
|
|
||||||
fw_ver: str | None = None
|
|
||||||
fw_type: str | None = None
|
|
||||||
is_main_sensor: bool | None = None
|
|
||||||
battery_voltage: int | None = None
|
|
||||||
humidity: int | None = None
|
|
||||||
temperature: float | None = None
|
|
||||||
model: str | None = None
|
|
||||||
rssi: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SensiboData:
|
|
||||||
"""Dataclass for Sensibo data."""
|
|
||||||
|
|
||||||
raw: dict
|
|
||||||
parsed: dict
|
|
||||||
|
|
||||||
|
|
||||||
class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
|
class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""A Sensibo Data Update Coordinator."""
|
"""A Sensibo Data Update Coordinator."""
|
||||||
|
@ -67,156 +39,13 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
async def _async_update_data(self) -> SensiboData:
|
async def _async_update_data(self) -> SensiboData:
|
||||||
"""Fetch data from Sensibo."""
|
"""Fetch data from Sensibo."""
|
||||||
|
|
||||||
devices = []
|
|
||||||
try:
|
try:
|
||||||
data = await self.client.async_get_devices()
|
data = await self.client.async_get_devices_data()
|
||||||
for dev in data["result"]:
|
|
||||||
devices.append(dev)
|
|
||||||
except AuthenticationError as error:
|
except AuthenticationError as error:
|
||||||
raise ConfigEntryAuthFailed from error
|
raise ConfigEntryAuthFailed from error
|
||||||
except SensiboError as error:
|
except SensiboError as error:
|
||||||
raise UpdateFailed from error
|
raise UpdateFailed from error
|
||||||
|
|
||||||
if not devices:
|
if not data.raw:
|
||||||
raise UpdateFailed("No devices found")
|
raise UpdateFailed("No devices found")
|
||||||
|
return data
|
||||||
device_data: dict[str, Any] = {}
|
|
||||||
for dev in devices:
|
|
||||||
unique_id = dev["id"]
|
|
||||||
mac = dev["macAddress"]
|
|
||||||
name = dev["room"]["name"]
|
|
||||||
temperature = dev["measurements"].get("temperature")
|
|
||||||
humidity = dev["measurements"].get("humidity")
|
|
||||||
ac_states = dev["acState"]
|
|
||||||
target_temperature = ac_states.get("targetTemperature")
|
|
||||||
hvac_mode = ac_states.get("mode")
|
|
||||||
running = ac_states.get("on")
|
|
||||||
fan_mode = ac_states.get("fanLevel")
|
|
||||||
swing_mode = ac_states.get("swing")
|
|
||||||
horizontal_swing_mode = ac_states.get("horizontalSwing")
|
|
||||||
light_mode = ac_states.get("light")
|
|
||||||
available = dev["connectionStatus"].get("isAlive", True)
|
|
||||||
capabilities = dev["remoteCapabilities"]
|
|
||||||
hvac_modes = list(capabilities["modes"])
|
|
||||||
if hvac_modes:
|
|
||||||
hvac_modes.append("off")
|
|
||||||
current_capabilities = capabilities["modes"][ac_states.get("mode")]
|
|
||||||
fan_modes = current_capabilities.get("fanLevels")
|
|
||||||
swing_modes = current_capabilities.get("swing")
|
|
||||||
horizontal_swing_modes = current_capabilities.get("horizontalSwing")
|
|
||||||
light_modes = current_capabilities.get("light")
|
|
||||||
temperature_unit_key = dev.get("temperatureUnit") or ac_states.get(
|
|
||||||
"temperatureUnit"
|
|
||||||
)
|
|
||||||
temperatures_list = (
|
|
||||||
current_capabilities["temperatures"]
|
|
||||||
.get(temperature_unit_key, {})
|
|
||||||
.get("values", [0, 1])
|
|
||||||
)
|
|
||||||
if temperatures_list:
|
|
||||||
diff = MAX_POSSIBLE_STEP
|
|
||||||
for i in range(len(temperatures_list) - 1):
|
|
||||||
if temperatures_list[i + 1] - temperatures_list[i] < diff:
|
|
||||||
diff = temperatures_list[i + 1] - temperatures_list[i]
|
|
||||||
temperature_step = diff
|
|
||||||
|
|
||||||
active_features = list(ac_states)
|
|
||||||
full_features = set()
|
|
||||||
for mode in capabilities["modes"]:
|
|
||||||
if "temperatures" in capabilities["modes"][mode]:
|
|
||||||
full_features.add("targetTemperature")
|
|
||||||
if "swing" in capabilities["modes"][mode]:
|
|
||||||
full_features.add("swing")
|
|
||||||
if "fanLevels" in capabilities["modes"][mode]:
|
|
||||||
full_features.add("fanLevel")
|
|
||||||
if "horizontalSwing" in capabilities["modes"][mode]:
|
|
||||||
full_features.add("horizontalSwing")
|
|
||||||
if "light" in capabilities["modes"][mode]:
|
|
||||||
full_features.add("light")
|
|
||||||
|
|
||||||
state = hvac_mode if hvac_mode else "off"
|
|
||||||
|
|
||||||
fw_ver = dev["firmwareVersion"]
|
|
||||||
fw_type = dev["firmwareType"]
|
|
||||||
model = dev["productModel"]
|
|
||||||
|
|
||||||
calibration_temp = dev["sensorsCalibration"].get("temperature")
|
|
||||||
calibration_hum = dev["sensorsCalibration"].get("humidity")
|
|
||||||
|
|
||||||
# Sky plus supports functionality to use motion sensor as sensor for temp and humidity
|
|
||||||
if main_sensor := dev["mainMeasurementsSensor"]:
|
|
||||||
measurements = main_sensor["measurements"]
|
|
||||||
temperature = measurements.get("temperature")
|
|
||||||
humidity = measurements.get("humidity")
|
|
||||||
|
|
||||||
motion_sensors: dict[str, Any] = {}
|
|
||||||
if dev["motionSensors"]:
|
|
||||||
for sensor in dev["motionSensors"]:
|
|
||||||
measurement = sensor["measurements"]
|
|
||||||
motion_sensors[sensor["id"]] = MotionSensor(
|
|
||||||
id=sensor["id"],
|
|
||||||
alive=sensor["connectionStatus"].get("isAlive"),
|
|
||||||
motion=measurement.get("motion"),
|
|
||||||
fw_ver=sensor.get("firmwareVersion"),
|
|
||||||
fw_type=sensor.get("firmwareType"),
|
|
||||||
is_main_sensor=sensor.get("isMainSensor"),
|
|
||||||
battery_voltage=measurement.get("batteryVoltage"),
|
|
||||||
humidity=measurement.get("humidity"),
|
|
||||||
temperature=measurement.get("temperature"),
|
|
||||||
model=sensor.get("productModel"),
|
|
||||||
rssi=measurement.get("rssi"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add information for pure devices
|
|
||||||
pure_conf = dev["pureBoostConfig"]
|
|
||||||
pure_sensitivity = pure_conf.get("sensitivity") if pure_conf else None
|
|
||||||
pure_boost_enabled = pure_conf.get("enabled") if pure_conf else None
|
|
||||||
pm25 = dev["measurements"].get("pm25")
|
|
||||||
|
|
||||||
# Binary sensors for main device
|
|
||||||
room_occupied = dev["roomIsOccupied"]
|
|
||||||
update_available = bool(
|
|
||||||
dev["firmwareVersion"] != dev["currentlyAvailableFirmwareVersion"]
|
|
||||||
)
|
|
||||||
|
|
||||||
device_data[unique_id] = {
|
|
||||||
"id": unique_id,
|
|
||||||
"mac": mac,
|
|
||||||
"name": name,
|
|
||||||
"ac_states": ac_states,
|
|
||||||
"temp": temperature,
|
|
||||||
"humidity": humidity,
|
|
||||||
"target_temp": target_temperature,
|
|
||||||
"hvac_mode": hvac_mode,
|
|
||||||
"on": running,
|
|
||||||
"fan_mode": fan_mode,
|
|
||||||
"swing_mode": swing_mode,
|
|
||||||
"horizontal_swing_mode": horizontal_swing_mode,
|
|
||||||
"light_mode": light_mode,
|
|
||||||
"available": available,
|
|
||||||
"hvac_modes": hvac_modes,
|
|
||||||
"fan_modes": fan_modes,
|
|
||||||
"swing_modes": swing_modes,
|
|
||||||
"horizontal_swing_modes": horizontal_swing_modes,
|
|
||||||
"light_modes": light_modes,
|
|
||||||
"temp_unit": temperature_unit_key,
|
|
||||||
"temp_list": temperatures_list,
|
|
||||||
"temp_step": temperature_step,
|
|
||||||
"active_features": active_features,
|
|
||||||
"full_features": full_features,
|
|
||||||
"state": state,
|
|
||||||
"fw_ver": fw_ver,
|
|
||||||
"fw_type": fw_type,
|
|
||||||
"model": model,
|
|
||||||
"calibration_temp": calibration_temp,
|
|
||||||
"calibration_hum": calibration_hum,
|
|
||||||
"full_capabilities": capabilities,
|
|
||||||
"motion_sensors": motion_sensors,
|
|
||||||
"pure_sensitivity": pure_sensitivity,
|
|
||||||
"pure_boost_enabled": pure_boost_enabled,
|
|
||||||
"pm25": pm25,
|
|
||||||
"room_occupied": room_occupied,
|
|
||||||
"update_available": update_available,
|
|
||||||
}
|
|
||||||
|
|
||||||
return SensiboData(raw=data, parsed=device_data)
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
from pysensibo.model import MotionSensor, SensiboDevice
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||||
|
@ -11,7 +12,7 @@ from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT
|
from .const import DOMAIN, LOGGER, SENSIBO_ERRORS, TIMEOUT
|
||||||
from .coordinator import MotionSensor, SensiboDataUpdateCoordinator
|
from .coordinator import SensiboDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
|
class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
|
||||||
|
@ -28,7 +29,7 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]):
|
||||||
self._client = coordinator.client
|
self._client = coordinator.client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_data(self) -> dict[str, Any]:
|
def device_data(self) -> SensiboDevice:
|
||||||
"""Return data for device."""
|
"""Return data for device."""
|
||||||
return self.coordinator.data.parsed[self._device_id]
|
return self.coordinator.data.parsed[self._device_id]
|
||||||
|
|
||||||
|
@ -44,15 +45,15 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity):
|
||||||
"""Initiate Sensibo Number."""
|
"""Initiate Sensibo Number."""
|
||||||
super().__init__(coordinator, device_id)
|
super().__init__(coordinator, device_id)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self.device_data["id"])},
|
identifiers={(DOMAIN, self.device_data.id)},
|
||||||
name=self.device_data["name"],
|
name=self.device_data.name,
|
||||||
connections={(CONNECTION_NETWORK_MAC, self.device_data["mac"])},
|
connections={(CONNECTION_NETWORK_MAC, self.device_data.mac)},
|
||||||
manufacturer="Sensibo",
|
manufacturer="Sensibo",
|
||||||
configuration_url="https://home.sensibo.com/",
|
configuration_url="https://home.sensibo.com/",
|
||||||
model=self.device_data["model"],
|
model=self.device_data.model,
|
||||||
sw_version=self.device_data["fw_ver"],
|
sw_version=self.device_data.fw_ver,
|
||||||
hw_version=self.device_data["fw_type"],
|
hw_version=self.device_data.fw_type,
|
||||||
suggested_area=self.device_data["name"],
|
suggested_area=self.device_data.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_send_command(
|
async def async_send_command(
|
||||||
|
@ -108,7 +109,7 @@ class SensiboMotionBaseEntity(SensiboBaseEntity):
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, sensor_id)},
|
identifiers={(DOMAIN, sensor_id)},
|
||||||
name=f"{self.device_data['name']} Motion Sensor {name}",
|
name=f"{self.device_data.name} Motion Sensor {name}",
|
||||||
via_device=(DOMAIN, device_id),
|
via_device=(DOMAIN, device_id),
|
||||||
manufacturer="Sensibo",
|
manufacturer="Sensibo",
|
||||||
configuration_url="https://home.sensibo.com/",
|
configuration_url="https://home.sensibo.com/",
|
||||||
|
@ -120,4 +121,4 @@ class SensiboMotionBaseEntity(SensiboBaseEntity):
|
||||||
@property
|
@property
|
||||||
def sensor_data(self) -> MotionSensor:
|
def sensor_data(self) -> MotionSensor:
|
||||||
"""Return data for device."""
|
"""Return data for device."""
|
||||||
return self.device_data["motion_sensors"][self._sensor_id]
|
return self.device_data.motion_sensors[self._sensor_id]
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "sensibo",
|
"domain": "sensibo",
|
||||||
"name": "Sensibo",
|
"name": "Sensibo",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/sensibo",
|
"documentation": "https://www.home-assistant.io/integrations/sensibo",
|
||||||
"requirements": ["pysensibo==1.0.8"],
|
"requirements": ["pysensibo==1.0.9"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"codeowners": ["@andrey-git", "@gjohansson-ST"],
|
"codeowners": ["@andrey-git", "@gjohansson-ST"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
|
|
|
@ -84,25 +84,19 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity):
|
||||||
super().__init__(coordinator, device_id)
|
super().__init__(coordinator, device_id)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
||||||
self._attr_name = (
|
self._attr_name = f"{self.device_data.name} {entity_description.name}"
|
||||||
f"{coordinator.data.parsed[device_id]['name']} {entity_description.name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self) -> float | None:
|
def value(self) -> float | None:
|
||||||
"""Return the value from coordinator data."""
|
"""Return the value from coordinator data."""
|
||||||
return self.coordinator.data.parsed[self._device_id][
|
return getattr(self.device_data, self.entity_description.key)
|
||||||
self.entity_description.key
|
|
||||||
]
|
|
||||||
|
|
||||||
async def async_set_value(self, value: float) -> None:
|
async def async_set_value(self, value: float) -> None:
|
||||||
"""Set value for calibration."""
|
"""Set value for calibration."""
|
||||||
data = {self.entity_description.remote_key: value}
|
data = {self.entity_description.remote_key: value}
|
||||||
result = await self.async_send_command("set_calibration", {"data": data})
|
result = await self.async_send_command("set_calibration", {"data": data})
|
||||||
if result["status"] == "success":
|
if result["status"] == "success":
|
||||||
self.coordinator.data.parsed[self._device_id][
|
setattr(self.device_data, self.entity_description.key, value)
|
||||||
self.entity_description.key
|
|
||||||
] = value
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
raise HomeAssistantError(f"Could not set calibration for device {self.name}")
|
raise HomeAssistantError(f"Could not set calibration for device {self.name}")
|
||||||
|
|
|
@ -58,7 +58,7 @@ async def async_setup_entry(
|
||||||
SensiboSelect(coordinator, device_id, description)
|
SensiboSelect(coordinator, device_id, description)
|
||||||
for device_id, device_data in coordinator.data.parsed.items()
|
for device_id, device_data in coordinator.data.parsed.items()
|
||||||
for description in SELECT_TYPES
|
for description in SELECT_TYPES
|
||||||
if description.key in device_data["full_features"]
|
if description.key in device_data.full_features
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,37 +77,35 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity):
|
||||||
super().__init__(coordinator, device_id)
|
super().__init__(coordinator, device_id)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
||||||
self._attr_name = (
|
self._attr_name = f"{self.device_data.name} {entity_description.name}"
|
||||||
f"{coordinator.data.parsed[device_id]['name']} {entity_description.name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_option(self) -> str | None:
|
def current_option(self) -> str | None:
|
||||||
"""Return the current selected option."""
|
"""Return the current selected option."""
|
||||||
return self.device_data[self.entity_description.remote_key]
|
return getattr(self.device_data, self.entity_description.remote_key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self) -> list[str]:
|
def options(self) -> list[str]:
|
||||||
"""Return possible options."""
|
"""Return possible options."""
|
||||||
return self.device_data[self.entity_description.remote_options] or []
|
return getattr(self.device_data, self.entity_description.remote_options) or []
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Set state to the selected option."""
|
"""Set state to the selected option."""
|
||||||
if self.entity_description.key not in self.device_data["active_features"]:
|
if self.entity_description.key not in self.device_data.active_features:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Current mode {self.device_data['hvac_mode']} doesn't support setting {self.entity_description.name}"
|
f"Current mode {self.device_data.hvac_mode} doesn't support setting {self.entity_description.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"name": self.entity_description.key,
|
"name": self.entity_description.key,
|
||||||
"value": option,
|
"value": option,
|
||||||
"ac_states": self.device_data["ac_states"],
|
"ac_states": self.device_data.ac_states,
|
||||||
"assumed_state": False,
|
"assumed_state": False,
|
||||||
}
|
}
|
||||||
result = await self.async_send_command("set_ac_state", params)
|
result = await self.async_send_command("set_ac_state", params)
|
||||||
|
|
||||||
if result["result"]["status"] == "Success":
|
if result["result"]["status"] == "Success":
|
||||||
self.device_data[self.entity_description.remote_key] = option
|
setattr(self.device_data, self.entity_description.remote_key, option)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
|
||||||
|
from pysensibo.model import MotionSensor, SensiboDevice
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -25,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import MotionSensor, SensiboDataUpdateCoordinator
|
from .coordinator import SensiboDataUpdateCoordinator
|
||||||
from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity
|
from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ class MotionBaseEntityDescriptionMixin:
|
||||||
class DeviceBaseEntityDescriptionMixin:
|
class DeviceBaseEntityDescriptionMixin:
|
||||||
"""Mixin for required Sensibo base description keys."""
|
"""Mixin for required Sensibo base description keys."""
|
||||||
|
|
||||||
value_fn: Callable[[dict[str, Any]], StateType]
|
value_fn: Callable[[SensiboDevice], StateType]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -106,13 +107,13 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = (
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
name="PM2.5",
|
name="PM2.5",
|
||||||
icon="mdi:air-filter",
|
icon="mdi:air-filter",
|
||||||
value_fn=lambda data: data["pm25"],
|
value_fn=lambda data: data.pm25,
|
||||||
),
|
),
|
||||||
SensiboDeviceSensorEntityDescription(
|
SensiboDeviceSensorEntityDescription(
|
||||||
key="pure_sensitivity",
|
key="pure_sensitivity",
|
||||||
name="Pure Sensitivity",
|
name="Pure Sensitivity",
|
||||||
icon="mdi:air-filter",
|
icon="mdi:air-filter",
|
||||||
value_fn=lambda data: data["pure_sensitivity"],
|
value_fn=lambda data: data.pure_sensitivity,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -129,15 +130,15 @@ async def async_setup_entry(
|
||||||
entities.extend(
|
entities.extend(
|
||||||
SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description)
|
SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description)
|
||||||
for device_id, device_data in coordinator.data.parsed.items()
|
for device_id, device_data in coordinator.data.parsed.items()
|
||||||
for sensor_id, sensor_data in device_data["motion_sensors"].items()
|
for sensor_id, sensor_data in device_data.motion_sensors.items()
|
||||||
for description in MOTION_SENSOR_TYPES
|
for description in MOTION_SENSOR_TYPES
|
||||||
if device_data["motion_sensors"]
|
if device_data.motion_sensors
|
||||||
)
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
SensiboDeviceSensor(coordinator, device_id, description)
|
SensiboDeviceSensor(coordinator, device_id, description)
|
||||||
for device_id, device_data in coordinator.data.parsed.items()
|
for device_id, device_data in coordinator.data.parsed.items()
|
||||||
for description in DEVICE_SENSOR_TYPES
|
for description in DEVICE_SENSOR_TYPES
|
||||||
if device_data[description.key] is not None
|
if getattr(device_data, description.key) is not None
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
@ -166,7 +167,7 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity):
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{sensor_id}-{entity_description.key}"
|
self._attr_unique_id = f"{sensor_id}-{entity_description.key}"
|
||||||
self._attr_name = (
|
self._attr_name = (
|
||||||
f"{self.device_data['name']} Motion Sensor {entity_description.name}"
|
f"{self.device_data.name} Motion Sensor {entity_description.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -193,7 +194,7 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity):
|
||||||
)
|
)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
self._attr_unique_id = f"{device_id}-{entity_description.key}"
|
||||||
self._attr_name = f"{self.device_data['name']} {entity_description.name}"
|
self._attr_name = f"{self.device_data.name} {entity_description.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
|
|
|
@ -1767,7 +1767,7 @@ pysaj==0.0.16
|
||||||
pysdcp==1
|
pysdcp==1
|
||||||
|
|
||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.0.8
|
pysensibo==1.0.9
|
||||||
|
|
||||||
# homeassistant.components.serial
|
# homeassistant.components.serial
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
|
|
|
@ -1169,7 +1169,7 @@ pyrituals==0.0.6
|
||||||
pyruckus==0.12
|
pyruckus==0.12
|
||||||
|
|
||||||
# homeassistant.components.sensibo
|
# homeassistant.components.sensibo
|
||||||
pysensibo==1.0.8
|
pysensibo==1.0.9
|
||||||
|
|
||||||
# homeassistant.components.serial
|
# homeassistant.components.serial
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue