From 05d86189fb37c05789089199eda6772a94b615ed Mon Sep 17 00:00:00 2001 From: maxyvon Date: Tue, 16 Apr 2024 11:38:15 -0400 Subject: [PATCH 1/5] =?UTF-8?q?Modif=20sur=20la=20m=C3=A9t=C3=A9o=20ext?= =?UTF-8?q?=C3=A9rieur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/hilo/const.py | 20 ++++++++++-- custom_components/hilo/sensor.py | 54 ++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 25 deletions(-) diff --git a/custom_components/hilo/const.py b/custom_components/hilo/const.py index 5d14074..b628063 100755 --- a/custom_components/hilo/const.py +++ b/custom_components/hilo/const.py @@ -71,8 +71,24 @@ TARIFF_LIST = ["high", "medium", "low"] +WEATHER_CONDITIONS = { + "Unknown": "mdi:weather-sunny-alert", + "BlowingSnow": "mdi:weather-snowy-heavy", + "Clear": "mdi:weather-sunny", + "Cloudy": "mdi:weather-cloudy", + "Fair": "mdi:weather-partly-cloudy", + "Foggy": "mdi:weather-fog", + "HailSleet": "mdi:weather-hail", + "MostlyCloudy": "mdi:weather-partly-cloudy", + "Rain": "mdi:weather-rainy", + "RainSnow": "mdi:weather-snowy-rainy", + "Snow": "mdi:weather-snowy", + "Thunder": "mdi:weather-lightning", + "Windy": "mdi:weather-windy", +} + # Class lists -LIGHT_CLASSES = ["LightDimmer", "WhiteBulb", "ColorBulb", "LightSwitch"] +LIGHT_CLASSES = ["LightDimmer", "WhiteBulb", "ColorBulb"] HILO_SENSOR_CLASSES = [ "SmokeDetector", "IndoorWeatherStation", @@ -80,4 +96,4 @@ "Gateway", ] CLIMATE_CLASSES = ["Thermostat", "FloorThermostat", "Thermostat24V"] -SWITCH_CLASSES = ["Outlet", "Ccr", "Cee"] +SWITCH_CLASSES = ["LightSwitch", "Outlet", "Ccr", "Cee"] diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 57a9b71..7ec815e 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -18,6 +18,7 @@ CURRENCY_DOLLAR, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + STATE_UNKNOWN, Platform, UnitOfEnergy, UnitOfPower, @@ -56,6 +57,7 @@ NOTIFICATION_SCAN_INTERVAL, REWARD_SCAN_INTERVAL, TARIFF_LIST, + WEATHER_CONDITIONS, ) from .managers import EnergyManager, UtilityManager @@ -100,7 +102,7 @@ def generate_entities_from_device(device, hilo, scan_interval): HiloNotificationSensor(hilo, device, scan_interval), ) entities.append( - HiloOutDoorTempSensor(hilo, device, scan_interval), + HiloOutdoorTempSensor(hilo, device, scan_interval), ) if device.has_attribute("battery"): entities.append(BatterySensor(hilo, device)) @@ -840,7 +842,10 @@ async def async_update(self): return -class HiloOutDoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): +#class HiloOutdoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): +# je ne crois pas qu'on a besoin d'un restoreentity pour une température. +# La dernière valeur n'a pas vraiment d'importance? +class HiloOutdoorTempSensor(HiloEntity, SensorEntity): """Hilo outdoor temperature sensor. Its state will be the current outdoor weather as reported by the Hilo App """ @@ -852,16 +857,18 @@ class HiloOutDoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): def __init__(self, hilo, device, scan_interval): self._attr_name = "Outdoor Weather Hilo" super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = slugify(self._attr_name) + #old_unique_id = slugify(self._attr_name) + #pas requis pusisqu'on l'a jamais créé avec un autre uniqueID + #par contre on peut laisser pour être comme les autres sensors self._attr_unique_id = ( f"{slugify(device.identifier)}-{slugify(self._attr_name)}" ) - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up OutDoorWeatherSensor entity: {self._attr_name}") + # hilo.async_migrate_unique_id( + # old_unique_id, self._attr_unique_id, Platform.SENSOR + # ) + LOG.debug(f"Setting up OutdoorWeatherSensor entity: {self._attr_name}") self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL_REDUCTION) - self._state = 0 + self._state = STATE_UNKNOWN self._weather = {} self.async_update = Throttle(self.scan_interval)(self._async_update) @@ -870,32 +877,33 @@ def state(self): try: return int(self._state) except ValueError: - return 0 + return STATE_UNKNOWN @property def icon(self): if not self._device.available: return "mdi:lan-disconnect" - if self.state > 0: - return "mdi:weather-sunny" - return "mdi:weather-sunny" - - # note(id-dev21): add quick if loop to change icon according to conditions here + return WEATHER_CONDITIONS.get(self._weather.get("condition", "Unknown")) + # le code est moins lourd en utilisant une constate, en plus on garde une constante similaire à Hilo @property def should_poll(self): return True @property def extra_state_attributes(self): - return {"weather": self._weather} - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: - self._last_update = dt_util.utcnow() - self._state = last_state.state + LOG.debug(f"Adding weather {self._weather}") + # Les attributes n'avait pas l'aire créé séparément mais plutot juste en une seule string + # J'ai enlevé temperature puis qu'elle était difini 2 fois dans le fond. + # J'ai enlevé icon puisque c'était 0 et je voulais pas que ça rentre en conflis avec celle de HA + return {key: self._weather[key] for key in self._weather if key not in ["temperature", "icon"]} + + # async def async_added_to_hass(self): + # """Handle entity about to be added to hass event.""" + # await super().async_added_to_hass() + # last_state = await self.async_get_last_state() + # if last_state: + # self._last_update = dt_util.utcnow() + # self._state = last_state.state async def _async_update(self): self._weather = {} From d7810a72433c969b0d2b02d23f23d8bbbee88862 Mon Sep 17 00:00:00 2001 From: maxyvon Date: Tue, 16 Apr 2024 13:13:07 -0400 Subject: [PATCH 2/5] Lint --- custom_components/hilo/const.py | 26 +++++++++++++------------- custom_components/hilo/sensor.py | 17 +++++++++++------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/custom_components/hilo/const.py b/custom_components/hilo/const.py index b628063..42ba0e8 100755 --- a/custom_components/hilo/const.py +++ b/custom_components/hilo/const.py @@ -72,19 +72,19 @@ TARIFF_LIST = ["high", "medium", "low"] WEATHER_CONDITIONS = { - "Unknown": "mdi:weather-sunny-alert", - "BlowingSnow": "mdi:weather-snowy-heavy", - "Clear": "mdi:weather-sunny", - "Cloudy": "mdi:weather-cloudy", - "Fair": "mdi:weather-partly-cloudy", - "Foggy": "mdi:weather-fog", - "HailSleet": "mdi:weather-hail", - "MostlyCloudy": "mdi:weather-partly-cloudy", - "Rain": "mdi:weather-rainy", - "RainSnow": "mdi:weather-snowy-rainy", - "Snow": "mdi:weather-snowy", - "Thunder": "mdi:weather-lightning", - "Windy": "mdi:weather-windy", + "Unknown": "mdi:weather-sunny-alert", + "BlowingSnow": "mdi:weather-snowy-heavy", + "Clear": "mdi:weather-sunny", + "Cloudy": "mdi:weather-cloudy", + "Fair": "mdi:weather-partly-cloudy", + "Foggy": "mdi:weather-fog", + "HailSleet": "mdi:weather-hail", + "MostlyCloudy": "mdi:weather-partly-cloudy", + "Rain": "mdi:weather-rainy", + "RainSnow": "mdi:weather-snowy-rainy", + "Snow": "mdi:weather-snowy", + "Thunder": "mdi:weather-lightning", + "Windy": "mdi:weather-windy", } # Class lists diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 7ec815e..938a804 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -842,7 +842,7 @@ async def async_update(self): return -#class HiloOutdoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): +# class HiloOutdoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): # je ne crois pas qu'on a besoin d'un restoreentity pour une température. # La dernière valeur n'a pas vraiment d'importance? class HiloOutdoorTempSensor(HiloEntity, SensorEntity): @@ -857,9 +857,9 @@ class HiloOutdoorTempSensor(HiloEntity, SensorEntity): def __init__(self, hilo, device, scan_interval): self._attr_name = "Outdoor Weather Hilo" super().__init__(hilo, name=self._attr_name, device=device) - #old_unique_id = slugify(self._attr_name) - #pas requis pusisqu'on l'a jamais créé avec un autre uniqueID - #par contre on peut laisser pour être comme les autres sensors + # old_unique_id = slugify(self._attr_name) + # pas requis pusisqu'on l'a jamais créé avec un autre uniqueID + # par contre on peut laisser pour être comme les autres sensors self._attr_unique_id = ( f"{slugify(device.identifier)}-{slugify(self._attr_name)}" ) @@ -884,7 +884,8 @@ def icon(self): if not self._device.available: return "mdi:lan-disconnect" return WEATHER_CONDITIONS.get(self._weather.get("condition", "Unknown")) - # le code est moins lourd en utilisant une constate, en plus on garde une constante similaire à Hilo + # le code est moins lourd en utilisant une constate, en plus on garde une constante similaire à Hilo + @property def should_poll(self): return True @@ -895,7 +896,11 @@ def extra_state_attributes(self): # Les attributes n'avait pas l'aire créé séparément mais plutot juste en une seule string # J'ai enlevé temperature puis qu'elle était difini 2 fois dans le fond. # J'ai enlevé icon puisque c'était 0 et je voulais pas que ça rentre en conflis avec celle de HA - return {key: self._weather[key] for key in self._weather if key not in ["temperature", "icon"]} + return { + key: self._weather[key] + for key in self._weather + if key not in ["temperature", "icon"] + } # async def async_added_to_hass(self): # """Handle entity about to be added to hass event.""" From f27e536a1aa1586d9c12c2466e12b019a4d39b9c Mon Sep 17 00:00:00 2001 From: maxyvon Date: Tue, 16 Apr 2024 15:48:18 -0400 Subject: [PATCH 3/5] merge conflicts --- custom_components/hilo/sensor.py | 918 ------------------------------- 1 file changed, 918 deletions(-) delete mode 100755 custom_components/hilo/sensor.py diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py deleted file mode 100755 index 938a804..0000000 --- a/custom_components/hilo/sensor.py +++ /dev/null @@ -1,918 +0,0 @@ -"""Support for various Hilo sensors.""" - -from __future__ import annotations - -from datetime import datetime, timedelta, timezone -from os.path import isfile - -from homeassistant.components.integration.sensor import METHOD_LEFT, IntegrationSensor -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - CONF_SCAN_INTERVAL, - CURRENCY_DOLLAR, - PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - STATE_UNKNOWN, - Platform, - UnitOfEnergy, - UnitOfPower, - UnitOfSoundPressure, - UnitOfTemperature, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import Throttle, slugify -import homeassistant.util.dt as dt_util -from pyhilo.const import UNMONITORED_DEVICES -from pyhilo.device import HiloDevice -from pyhilo.event import Event -from pyhilo.util import from_utc_timestamp -import ruyaml as yaml - -from . import Hilo, HiloEntity -from .const import ( - CONF_ENERGY_METER_PERIOD, - CONF_GENERATE_ENERGY_METERS, - CONF_HQ_PLAN_NAME, - CONF_TARIFF, - CONF_UNTARIFICATED_DEVICES, - DEFAULT_ENERGY_METER_PERIOD, - DEFAULT_GENERATE_ENERGY_METERS, - DEFAULT_HQ_PLAN_NAME, - DEFAULT_SCAN_INTERVAL, - DEFAULT_UNTARIFICATED_DEVICES, - DOMAIN, - EVENT_SCAN_INTERVAL_REDUCTION, - HILO_ENERGY_TOTAL, - HILO_SENSOR_CLASSES, - LOG, - NOTIFICATION_SCAN_INTERVAL, - REWARD_SCAN_INTERVAL, - TARIFF_LIST, - WEATHER_CONDITIONS, -) -from .managers import EnergyManager, UtilityManager - -WIFI_STRENGTH = { - "Low": 1, - "Medium": 2, - "High": 3, - "Full": 4, -} - - -# From netatmo integration -def process_wifi(strength: int) -> str: - """Process Wi-Fi signal strength and return string for display.""" - if strength >= 86: - return "Low" - if strength >= 71: - return "Medium" - if strength >= 56: - return "High" - return "Full" - - -def validate_tariff_list(tariff_config): - tariff_list = TARIFF_LIST - for tariff in TARIFF_LIST: - if not tariff_config.get(tariff, 0): - tariff_list.remove(tariff) - return tariff_list - - -def generate_entities_from_device(device, hilo, scan_interval): - entities = [] - if device.type == "Gateway": - entities.append( - HiloChallengeSensor(hilo, device, scan_interval), - ) - entities.append( - HiloRewardSensor(hilo, device, scan_interval), - ) - entities.append( - HiloNotificationSensor(hilo, device, scan_interval), - ) - entities.append( - HiloOutdoorTempSensor(hilo, device, scan_interval), - ) - if device.has_attribute("battery"): - entities.append(BatterySensor(hilo, device)) - if device.has_attribute("co2"): - entities.append(Co2Sensor(hilo, device)) - if device.has_attribute("current_temperature"): - entities.append(TemperatureSensor(hilo, device)) - if device.type in HILO_SENSOR_CLASSES: - entities.append(DeviceSensor(hilo, device)) - if device.has_attribute("noise"): - entities.append(NoiseSensor(hilo, device)) - if device.has_attribute("power") and device.model not in UNMONITORED_DEVICES: - entities.append(PowerSensor(hilo, device)) - if device.has_attribute("target_temperature"): - entities.append(TargetTemperatureSensor(hilo, device)) - if device.has_attribute("wifi_status"): - entities.append(WifiStrengthSensor(hilo, device)) - return entities - - -# noinspection GrazieInspection -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up Hilo sensors based on a config entry.""" - hilo = hass.data[DOMAIN][entry.entry_id] - new_entities = [] - cost_entities = [] - hq_plan_name = entry.options.get(CONF_HQ_PLAN_NAME, DEFAULT_HQ_PLAN_NAME) - untarificated_devices = entry.options.get( - CONF_UNTARIFICATED_DEVICES, DEFAULT_UNTARIFICATED_DEVICES - ) - energy_meter_period = entry.options.get( - CONF_ENERGY_METER_PERIOD, DEFAULT_ENERGY_METER_PERIOD - ) - scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - generate_energy_meters = entry.options.get( - CONF_GENERATE_ENERGY_METERS, DEFAULT_GENERATE_ENERGY_METERS - ) - tariff_config = CONF_TARIFF.get(hq_plan_name) - if untarificated_devices: - default_tariff_list = ["total"] - else: - default_tariff_list = validate_tariff_list(tariff_config) - if generate_energy_meters: - energy_manager = await EnergyManager().init(hass, energy_meter_period) - utility_manager = UtilityManager(hass, energy_meter_period, default_tariff_list) - - def create_energy_entity(hilo, device): - device._energy_entity = EnergySensor(hilo, device) - new_entities.append(device._energy_entity) - energy_entity = f"{slugify(device.name)}_hilo_energy" - if energy_entity == HILO_ENERGY_TOTAL: - LOG.error( - "An hilo entity can't be named 'total' because it conflicts " - "with the generated name for the smart energy meter" - ) - return - tariff_list = default_tariff_list - if device.type == "Meter": - energy_entity = HILO_ENERGY_TOTAL - tariff_list = validate_tariff_list(tariff_config) - net_consumption = device.net_consumption - utility_manager.add_meter(energy_entity, tariff_list, net_consumption) - - for d in hilo.devices.all: - LOG.debug(f"Adding device {d}") - new_entities.extend(generate_entities_from_device(d, hilo, scan_interval)) - if d.has_attribute("power") and d.model not in UNMONITORED_DEVICES: - # If we opt out the generation of meters we just create the power sensors - if generate_energy_meters: - create_energy_entity(hilo, d) - - async_add_entities(new_entities) - if not generate_energy_meters: - return - # Creating cost sensors based on plan - # This will generate hilo_cost_(low|medium|high) sensors which can be - # referred later in the energy dashboard based on the tarif selected - for tarif, amount in tariff_config.items(): - if amount > 0: - sensor_name = f"Hilo rate {tarif}" - cost_entities.append( - HiloCostSensor(hilo, sensor_name, hq_plan_name, amount) - ) - cost_entities.append(HiloCostSensor(hilo, "Hilo rate current", hq_plan_name)) - async_add_entities(cost_entities) - # This setups the utility_meter platform - await utility_manager.update(async_add_entities) - # This sends the entities to the energy dashboard - await energy_manager.update() - - -class BatterySensor(HiloEntity, SensorEntity): - """Define a Battery sensor entity.""" - - _attr_device_class = SensorDeviceClass.BATTERY - _attr_native_unit_of_measurement = PERCENTAGE - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device): - self._attr_name = f"{device.name} Battery" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = f"{slugify(device.name)}-battery" - self._attr_unique_id = f"{slugify(device.identifier)}-battery" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up BatterySensor entity: {self._attr_name}") - - @property - def state(self): - return str(int(self._device.get_value("battery", 0))) - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - level = round(int(self._device.get_value("battery", 0)) / 10) * 10 - if level < 10: - return "mdi:battery-alert" - return f"mdi:battery-{level}" - - -class Co2Sensor(HiloEntity, SensorEntity): - """Define a Co2 sensor entity.""" - - _attr_device_class = SensorDeviceClass.CO2 - _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device): - self._attr_name = f"{device.name} CO2" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = f"{slugify(device.name)}-co2" - self._attr_unique_id = f"{slugify(device.identifier)}-co2" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up CO2Sensor entity: {self._attr_name}") - - @property - def state(self): - return str(int(self._device.get_value("co2", 0))) - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - return "mdi:molecule-co2" - - -class EnergySensor(IntegrationSensor): - """Define a Hilo energy sensor entity.""" - - _attr_device_class = SensorDeviceClass.ENERGY - _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - _attr_state_class = SensorStateClass.TOTAL_INCREASING - _attr_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - - def __init__(self, hilo, device): - self._device = device - self._attr_name = f"{device.name} Hilo Energy" - old_unique_id = f"hilo_energy_{slugify(device.name)}" - self._attr_unique_id = f"{slugify(device.identifier)}-energy" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - self._unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - self._unit_prefix = None - - if device.type == "Meter": - self._attr_name = HILO_ENERGY_TOTAL - self._source = f"sensor.{slugify(device.name)}_power" - # ic-dev21: Set initial state and last_valid_state, removes log errors and unavailable states - initial_state = 0 - self._attr_native_value = initial_state - self._attr_last_valid_state = initial_state - self._device_info = DeviceInfo( - identifiers={(DOMAIN, self._device.identifier)}, - ) - - super().__init__( - integration_method=METHOD_LEFT, - name=self._attr_name, - round_digits=2, - source_entity=self._source, - unique_id=self._attr_unique_id, - unit_prefix="k", - unit_time="h", - device_info=self._device_info, - ) - self._attr_icon = "mdi:lightning-bolt" - LOG.debug( - f"Setting up EnergySensor entity: {self._attr_name} with source {self._source}" - ) - - @property - def unit_of_measurement(self): - return self._attr_unit_of_measurement - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - LOG.debug(f"Added to hass: {self._attr_name}") - await super().async_added_to_hass() - - -class NoiseSensor(HiloEntity, SensorEntity): - """Define a Netatmo noise sensor entity.""" - - _attr_native_unit_of_measurement = UnitOfSoundPressure.DECIBEL - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device): - self._attr_name = f"{device.name} Noise" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = f"{slugify(device.name)}-noise" - self._attr_unique_id = f"{slugify(device.identifier)}-noise" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up NoiseSensor entity: {self._attr_name}") - - @property - def state(self): - return str(int(self._device.get_value("noise", 0))) - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - if int(self._device.get_value("noise", 0)) > 0: - return "mdi:volume-vibrate" - return "mdi:volume-mute" - - -class PowerSensor(HiloEntity, SensorEntity): - """Define a Hilo power sensor entity.""" - - _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = UnitOfPower.WATT - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo: Hilo, device: HiloDevice) -> None: - """Initialize.""" - self._attr_name = f"{device.name} Power" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = f"{slugify(device.name)}-power" - self._attr_unique_id = f"{slugify(device.identifier)}-power" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up PowerSensor entity: {self._attr_name}") - - @property - def state(self): - return str(int(self._device.get_value("power", 0))) - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - power = int(self._device.get_value("power", 0)) - if power > 0: - return "mdi:power-plug" - return "mdi:power-plug-off" - - -class TemperatureSensor(HiloEntity, SensorEntity): - """Define a Hilo temperature sensor entity.""" - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device): - self._attr_name = f"{device.name} Temperature" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = f"{slugify(device.name)}-temperature" - self._attr_unique_id = f"{slugify(device.identifier)}-temperature" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up TemperatureSensor entity: {self._attr_name}") - - @property - def state(self): - return str(float(self._device.get_value("current_temperature", 0))) - - @property - def icon(self): - current_temperature = int(self._device.get_value("current_temperature", 0)) - if not self._device.available: - thermometer = "off" - elif current_temperature >= 22: - thermometer = "high" - elif current_temperature >= 18: - thermometer = "low" - else: - thermometer = "alert" - return f"mdi:thermometer-{thermometer}" - - -class TargetTemperatureSensor(HiloEntity, SensorEntity): - """Define a Hilo target temperature sensor entity.""" - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device): - self._attr_name = f"{device.name} Target Temperature" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = f"{slugify(device.name)}-target-temperature" - self._attr_unique_id = f"{slugify(device.identifier)}-target-temperature" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up TargetTemperatureSensor entity: {self._attr_name}") - - @property - def state(self): - return str(float(self._device.get_value("target_temperature", 0))) - - @property - def icon(self): - target_temperature = int(self._device.get_value("target_temperature", 0)) - if not self._device.available: - thermometer = "off" - elif target_temperature >= 22: - thermometer = "high" - elif target_temperature >= 18: - thermometer = "low" - else: - thermometer = "alert" - return f"mdi:thermometer-{thermometer}" - - -class WifiStrengthSensor(HiloEntity, SensorEntity): - """Define a Wi-Fi strength sensor entity.""" - - _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH - _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device): - self._attr_name = f"{device.name} WifiStrength" - super().__init__(hilo, name=self._attr_name, device=device) - self._attr_unique_id = f"{slugify(device.name)}-wifistrength" - LOG.debug(f"Setting up WifiStrengthSensor entity: {self._attr_name}") - - @property - def state(self): - return process_wifi(self._device.get_value("wifi_status", 0)) - - @property - def icon(self): - if not self._device.available or self._device.get_value("wifi_status", 0) == 0: - return "mdi:wifi-strength-off" - return f"mdi:wifi-strength-{WIFI_STRENGTH[self.state]}" - - @property - def extra_state_attributes(self): - return {"wifi_signal": self._device.get_value("wifi_status", 0)} - - -class HiloNotificationSensor(HiloEntity, RestoreEntity, SensorEntity): - """Hilo Notification sensor. - Its state will be the number of notification waiting in the Hilo app. - Notifications only used for OneLink's alerts & Low-battery warnings. - We should consider having this sensor enabled only if a smoke detector is in use. - """ - - def __init__(self, hilo, device, scan_interval): - self._attr_name = "Notifications Hilo" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = slugify(self._attr_name) - self._attr_unique_id = ( - f"{slugify(device.identifier)}-{slugify(self._attr_name)}" - ) - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up NotificationSensor entity: {self._attr_name}") - self.scan_interval = timedelta(seconds=NOTIFICATION_SCAN_INTERVAL) - self._state = 0 - self._notifications = [] - self.async_update = Throttle(self.scan_interval)(self._async_update) - - @property - def state(self): - try: - return int(self._state) - except ValueError: - return 0 - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - if self.state > 0: - return "mdi:bell-alert" - return "mdi:bell-outline" - - @property - def should_poll(self): - return True - - @property - def extra_state_attributes(self): - return {"notifications": self._notifications} - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: - self._last_update = dt_util.utcnow() - self._state = last_state.state - - async def _async_update(self): - self._notifications = [] - for notification in await self._hilo._api.get_event_notifications( - self._hilo.devices.location_id - ): - if notification.get("viewed"): - continue - self._notifications.append( - { - "type_id": notification.get("eventTypeId"), - "event_id": notification.get("eventId"), - "device_id": notification.get("deviceId"), - "date": from_utc_timestamp(notification.get("notificationDateUTC")), - "title": notification.get("notificationTitle"), - "body": notification.get("notificationBody"), - } - ) - self._state = len(self._notifications) - - -class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity): - """Hilo Reward sensor. - Its state will be either the total amount rewarded this season. - """ - - _attr_device_class = SensorDeviceClass.MONETARY - _attr_state_class = SensorStateClass.TOTAL_INCREASING - _entity_component_unrecorded_attributes = frozenset({"history"}) - - def __init__(self, hilo, device, scan_interval): - self._attr_name = "Recompenses Hilo" - - # Check if currency is configured, set a default if not - currency = hilo._hass.config.currency - if currency: - self._attr_native_unit_of_measurement = currency - else: - # Set a default currency or handle the case where currency is not configured - self._attr_native_unit_of_measurement = "CAD" - - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = slugify(self._attr_name) - self._attr_unique_id = ( - f"{slugify(device.identifier)}-{slugify(self._attr_name)}" - ) - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up RewardSensor entity: {self._attr_name}") - self._history_state_yaml: str = "hilo_eventhistory_state.yaml" - self.scan_interval = timedelta(seconds=REWARD_SCAN_INTERVAL) - self._state = 0 - self._history = self._load_history() - self.async_update = Throttle(self.scan_interval)(self._async_update) - - @property - def state(self): - return self._state - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - return "mdi:cash-plus" - - @property - def should_poll(self): - return True - - @property - def extra_state_attributes(self): - return {"history": self._history} - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: - self._last_update = dt_util.utcnow() - self._state = last_state.state - - async def _async_update(self): - seasons = await self._hilo._api.get_seasons(self._hilo.devices.location_id) - if seasons: - current_history = self._history - new_history = [] - - for idx, season in enumerate(seasons): - current_history_season = next( - ( - item - for item in current_history - if item.get("season") == season.get("season") - ), - None, - ) - - if idx == 0: - self._state = season.get("totalReward", 0) - events = [] - for raw_event in season.get("events", []): - current_history_event = None - event = None - - if current_history_season: - current_history_event = next( - ( - ev - for ev in current_history_season["events"] - if ev["event_id"] == raw_event["id"] - ), - None, - ) - - start_date_utc = datetime.fromisoformat(raw_event["startDateUtc"]) - event_age = datetime.now(timezone.utc) - start_date_utc - if ( - current_history_event - and current_history_event.get("state") == "completed" - and event_age > timedelta(days=1) - ): - # No point updating events for previously completed events, they won't change. - event = current_history_event - else: - details = await self._hilo.get_event_details(raw_event["id"]) - event = Event(**details).as_dict() - - events.append(event) - season["events"] = events - new_history.append(season) - self._history = new_history - self._save_history(new_history) - - def _load_history(self) -> list: - history: list = [] - if isfile(self._history_state_yaml): - with open(self._history_state_yaml) as yaml_file: - LOG.debug("Loading history state from yaml") - history = yaml.load(yaml_file, Loader=yaml.Loader) - return history - - def _save_history(self, history: list): - with open(self._history_state_yaml, "w") as yaml_file: - LOG.debug("Saving history state to yaml file") - yaml.dump(history, yaml_file, Dumper=yaml.RoundTripDumper) - - -class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity): - """Hilo challenge sensor. - Its state will be either: - - off: no ongoing or scheduled challenge - - scheduled: A challenge is scheduled, details in the next_events - extra attribute - - pre_cold: optional phase to cool further before appreciation - - appreciation: optional phase to pre-heat more before challenge - - pre_heat: Currently in the pre-heat phase - - reduction or on: Challenge is currently active, heat is lowered - - recovery: Challenge is completed, we're reheating. - """ - - def __init__(self, hilo, device, scan_interval): - self._attr_name = "Defi Hilo" - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = slugify(self._attr_name) - self._attr_unique_id = ( - f"{slugify(device.identifier)}-{slugify(self._attr_name)}" - ) - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up ChallengeSensor entity: {self._attr_name}") - # note ic-dev21: scan time at 5 minutes (300s) will force local update - self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL_REDUCTION) - self._state = "off" - self._next_events = [] - self.async_update = Throttle(self.scan_interval)(self._async_update) - - @property - def state(self): - if len(self._next_events) > 0: - event = Event(**{**{"id": 0}, **self._next_events[0]}) - return event.state - else: - return "off" - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - if self.state == "appreciation": - return "mdi:glass-cocktail" - if self.state == "off": - return "mdi:lightning-bolt" - if self.state == "scheduled": - return "mdi:progress-clock" - if self.state == "pre_heat": - return "mdi:radiator" - if self.state in ["reduction", "on"]: - return "mdi:power-plug-off" - if self.state == "recovery": - return "mdi:calendar-check" - if self.state == "pre_cold": - return "mdi:radiator-off" - return "mdi:battery-alert" - - @property - def should_poll(self): - return True - - @property - def extra_state_attributes(self): - return {"next_events": self._next_events} - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - last_state = await self.async_get_last_state() - if last_state: - self._last_update = dt_util.utcnow() - self._state = last_state.state - self._next_events = last_state.attributes.get("next_events", []) - - async def _async_update(self): - self._next_events = [] - events = await self._hilo._api.get_gd_events(self._hilo.devices.location_id) - LOG.debug(f"Events received from Hilo: {events}") - for raw_event in events: - details = await self._hilo.get_event_details(raw_event["id"]) - event = Event(**details) - if self._hilo.appreciation > 0: - event.appreciation(self._hilo.appreciation) - if self._hilo.pre_cold > 0: - event.pre_cold(self._hilo.pre_cold) - self._next_events.append(event.as_dict()) - - -class DeviceSensor(HiloEntity, SensorEntity): - """Devices like the gateway or Smoke Detectors don't have many attributes, - except for the "disconnected" attribute. These entities are monitoring - this state. - """ - - def __init__(self, hilo, device): - self._attr_name = device.name - super().__init__(hilo, name=self._attr_name, device=device) - old_unique_id = slugify(device.name) - self._attr_unique_id = f"{slugify(device.identifier)}-{slugify(device.name)}" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - LOG.debug(f"Setting up DeviceSensor entity: {self._attr_name}") - - @property - def state(self): - return "on" if self._device.available else "off" - - @property - def extra_state_attributes(self): - return {k: self._device.get_value(k) for k in self._device.attributes} - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - if self.state == "off": - return "mdi:access-point-network-off" - return "mdi:access-point-network" - - -class HiloCostSensor(HiloEntity, RestoreEntity, SensorEntity): - _attr_device_class = SensorDeviceClass.MONETARY - _attr_native_unit_of_measurement = ( - f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" - ) - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_icon = "mdi:cash" - - def __init__(self, hilo, name, plan_name, amount=0): - for d in hilo.devices.all: - if d.type == "Gateway": - device = d - if "low_threshold" in name: - self._attr_device_class = SensorDeviceClass.ENERGY - self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - self.data = None - self._attr_name = name - self.plan_name = plan_name - self._amount = amount - old_unique_id = slugify(self._attr_name) - self._attr_unique_id = ( - f"{slugify(device.identifier)}-{slugify(self._attr_name)}" - ) - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SENSOR - ) - self._last_update = dt_util.utcnow() - super().__init__(hilo, name=self._attr_name, device=device) - LOG.info(f"Initializing energy cost sensor {name} {plan_name} Amount: {amount}") - - @property - def state(self): - return self._amount - - @property - def should_poll(self) -> bool: - return False - - @property - def extra_state_attributes(self): - return {"last_update": self._last_update, "Cost": self.state} - - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() - - async def async_update(self): - return - - -# class HiloOutdoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): -# je ne crois pas qu'on a besoin d'un restoreentity pour une température. -# La dernière valeur n'a pas vraiment d'importance? -class HiloOutdoorTempSensor(HiloEntity, SensorEntity): - """Hilo outdoor temperature sensor. - Its state will be the current outdoor weather as reported by the Hilo App - """ - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__(self, hilo, device, scan_interval): - self._attr_name = "Outdoor Weather Hilo" - super().__init__(hilo, name=self._attr_name, device=device) - # old_unique_id = slugify(self._attr_name) - # pas requis pusisqu'on l'a jamais créé avec un autre uniqueID - # par contre on peut laisser pour être comme les autres sensors - self._attr_unique_id = ( - f"{slugify(device.identifier)}-{slugify(self._attr_name)}" - ) - # hilo.async_migrate_unique_id( - # old_unique_id, self._attr_unique_id, Platform.SENSOR - # ) - LOG.debug(f"Setting up OutdoorWeatherSensor entity: {self._attr_name}") - self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL_REDUCTION) - self._state = STATE_UNKNOWN - self._weather = {} - self.async_update = Throttle(self.scan_interval)(self._async_update) - - @property - def state(self): - try: - return int(self._state) - except ValueError: - return STATE_UNKNOWN - - @property - def icon(self): - if not self._device.available: - return "mdi:lan-disconnect" - return WEATHER_CONDITIONS.get(self._weather.get("condition", "Unknown")) - # le code est moins lourd en utilisant une constate, en plus on garde une constante similaire à Hilo - - @property - def should_poll(self): - return True - - @property - def extra_state_attributes(self): - LOG.debug(f"Adding weather {self._weather}") - # Les attributes n'avait pas l'aire créé séparément mais plutot juste en une seule string - # J'ai enlevé temperature puis qu'elle était difini 2 fois dans le fond. - # J'ai enlevé icon puisque c'était 0 et je voulais pas que ça rentre en conflis avec celle de HA - return { - key: self._weather[key] - for key in self._weather - if key not in ["temperature", "icon"] - } - - # async def async_added_to_hass(self): - # """Handle entity about to be added to hass event.""" - # await super().async_added_to_hass() - # last_state = await self.async_get_last_state() - # if last_state: - # self._last_update = dt_util.utcnow() - # self._state = last_state.state - - async def _async_update(self): - self._weather = {} - self._weather = await self._hilo._api.get_weather( - self._hilo.devices.location_id - ) - self._state = self._weather.get("temperature") From 38ae70cecd57a902f466709a66facce9f38a74b9 Mon Sep 17 00:00:00 2001 From: maxyvon Date: Tue, 16 Apr 2024 15:49:05 -0400 Subject: [PATCH 4/5] merge conflicts --- README.en.md | 6 +- README.md | 8 +- custom_components/hilo/sensor.py | 920 +++++++++++++++++++++++++++++++ 3 files changed, 929 insertions(+), 5 deletions(-) create mode 100755 custom_components/hilo/sensor.py diff --git a/README.en.md b/README.en.md index d3d6e2c..d1c881f 100644 --- a/README.en.md +++ b/README.en.md @@ -51,8 +51,10 @@ rewrite it. Hilo is now pushing device readings via websocket from SignalR. - Generates energy meters and sensors - Sensor for Hilo Events (challenges) - Sensor for Hilo Gateway -- **NEW**: Now configuration is done via the UI -- **NEW**: Updates are now closer to realtime +- Now configuration is done via the UI +- Updates are now closer to realtime +- **NEW**: Authentication directly on Hilo's website +- **NEW**: Outdoor weather sensor with changing icon like in the Hilo App ### To Do: - Add functionalities for other devices diff --git a/README.md b/README.md index 369bffc..57b637a 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,10 @@ J'ai décidé de déplacer l'intégration ici, car la dernière mise à jour de - Générer les "sensor" de puissance et d'énergie consommée. - Sensor pour les Défis. - Sensor pour la passerelle Hilo -- **NOUVEAU**: Configuration est maintenant faite via l'interface utilisateur -- **NOUVEAU**: Mise à jour des lectures plus près du temps réel. +- Configuration est maintenant faite via l'interface utilisateur +- Mise à jour des lectures plus près du temps réel. +- **NOUVEAU**: Authentification via le site de Hilo +- **NOUVEAU**: Capteur pour la météo extérieure avec icône changeante comme dans l'app Hilo ### À faire: - Ajouter la fonctionnalité pour d'autres appareils. @@ -79,7 +81,7 @@ Dans le coin inférieur droit, cliquer sur le bouton '+ AJOUTER UNE INTÉGRATION ![Ajout intégration](https://github.com/dvd-dev/hilo/assets/108159253/e0529aca-9b13-40e0-9be4-29e347b980ab) -Si l'intégration est correctement installée, vous devriez pouvoir trouver "Hilo" dans la list. Il est possible d'avoir besoin de vider la mémoire cache de votre navigateur pour que l'intégration s'affiche. +Si l'intégration est correctement installée, vous devriez pouvoir trouver "Hilo" dans la liste. Il est possible d'avoir besoin de vider la mémoire cache de votre navigateur pour que l'intégration s'affiche. ![Recherche intégration](https://github.com/dvd-dev/hilo/assets/108159253/7003a402-9369-4063-ac02-709bd0294e42) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py new file mode 100755 index 0000000..f303aa6 --- /dev/null +++ b/custom_components/hilo/sensor.py @@ -0,0 +1,920 @@ +"""Support for various Hilo sensors.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone +from os.path import isfile + +from homeassistant.components.integration.sensor import METHOD_LEFT, IntegrationSensor +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + CONF_SCAN_INTERVAL, + CURRENCY_DOLLAR, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + STATE_UNKNOWN, + Platform, + UnitOfEnergy, + UnitOfPower, + UnitOfSoundPressure, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import Throttle, slugify +import homeassistant.util.dt as dt_util +from pyhilo.const import UNMONITORED_DEVICES +from pyhilo.device import HiloDevice +from pyhilo.event import Event +from pyhilo.util import from_utc_timestamp +import ruyaml as yaml + +from . import Hilo, HiloEntity +from .const import ( + CONF_ENERGY_METER_PERIOD, + CONF_GENERATE_ENERGY_METERS, + CONF_HQ_PLAN_NAME, + CONF_TARIFF, + CONF_UNTARIFICATED_DEVICES, + DEFAULT_ENERGY_METER_PERIOD, + DEFAULT_GENERATE_ENERGY_METERS, + DEFAULT_HQ_PLAN_NAME, + DEFAULT_SCAN_INTERVAL, + DEFAULT_UNTARIFICATED_DEVICES, + DOMAIN, + EVENT_SCAN_INTERVAL_REDUCTION, + HILO_ENERGY_TOTAL, + HILO_SENSOR_CLASSES, + LOG, + NOTIFICATION_SCAN_INTERVAL, + REWARD_SCAN_INTERVAL, + TARIFF_LIST, + WEATHER_CONDITIONS, +) +from .managers import EnergyManager, UtilityManager + +WIFI_STRENGTH = { + "Low": 1, + "Medium": 2, + "High": 3, + "Full": 4, +} + + +# From netatmo integration +def process_wifi(strength: int) -> str: + """Process Wi-Fi signal strength and return string for display.""" + if strength >= 86: + return "Low" + if strength >= 71: + return "Medium" + if strength >= 56: + return "High" + return "Full" + + +def validate_tariff_list(tariff_config): + tariff_list = TARIFF_LIST + for tariff in TARIFF_LIST: + if not tariff_config.get(tariff, 0): + tariff_list.remove(tariff) + return tariff_list + + +def generate_entities_from_device(device, hilo, scan_interval): + entities = [] + if device.type == "Gateway": + entities.append( + HiloChallengeSensor(hilo, device, scan_interval), + ) + entities.append( + HiloRewardSensor(hilo, device, scan_interval), + ) + entities.append( + HiloNotificationSensor(hilo, device, scan_interval), + ) + entities.append( + HiloOutdoorTempSensor(hilo, device, scan_interval), + ) + if device.has_attribute("battery"): + entities.append(BatterySensor(hilo, device)) + if device.has_attribute("co2"): + entities.append(Co2Sensor(hilo, device)) + if device.has_attribute("current_temperature"): + entities.append(TemperatureSensor(hilo, device)) + if device.type in HILO_SENSOR_CLASSES: + entities.append(DeviceSensor(hilo, device)) + if device.has_attribute("noise"): + entities.append(NoiseSensor(hilo, device)) + if device.has_attribute("power") and device.model not in UNMONITORED_DEVICES: + entities.append(PowerSensor(hilo, device)) + if device.has_attribute("target_temperature"): + entities.append(TargetTemperatureSensor(hilo, device)) + if device.has_attribute("wifi_status"): + entities.append(WifiStrengthSensor(hilo, device)) + return entities + + +# noinspection GrazieInspection +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Hilo sensors based on a config entry.""" + hilo = hass.data[DOMAIN][entry.entry_id] + new_entities = [] + cost_entities = [] + hq_plan_name = entry.options.get(CONF_HQ_PLAN_NAME, DEFAULT_HQ_PLAN_NAME) + untarificated_devices = entry.options.get( + CONF_UNTARIFICATED_DEVICES, DEFAULT_UNTARIFICATED_DEVICES + ) + energy_meter_period = entry.options.get( + CONF_ENERGY_METER_PERIOD, DEFAULT_ENERGY_METER_PERIOD + ) + scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + generate_energy_meters = entry.options.get( + CONF_GENERATE_ENERGY_METERS, DEFAULT_GENERATE_ENERGY_METERS + ) + tariff_config = CONF_TARIFF.get(hq_plan_name) + if untarificated_devices: + default_tariff_list = ["total"] + else: + default_tariff_list = validate_tariff_list(tariff_config) + if generate_energy_meters: + energy_manager = await EnergyManager().init(hass, energy_meter_period) + utility_manager = UtilityManager(hass, energy_meter_period, default_tariff_list) + + def create_energy_entity(hilo, device): + device._energy_entity = EnergySensor(hilo, device) + new_entities.append(device._energy_entity) + energy_entity = f"{slugify(device.name)}_hilo_energy" + if energy_entity == HILO_ENERGY_TOTAL: + LOG.error( + "An hilo entity can't be named 'total' because it conflicts " + "with the generated name for the smart energy meter" + ) + return + tariff_list = default_tariff_list + if device.type == "Meter": + energy_entity = HILO_ENERGY_TOTAL + tariff_list = validate_tariff_list(tariff_config) + net_consumption = device.net_consumption + utility_manager.add_meter(energy_entity, tariff_list, net_consumption) + + for d in hilo.devices.all: + LOG.debug(f"Adding device {d}") + new_entities.extend(generate_entities_from_device(d, hilo, scan_interval)) + if d.has_attribute("power") and d.model not in UNMONITORED_DEVICES: + # If we opt out the generation of meters we just create the power sensors + if generate_energy_meters: + create_energy_entity(hilo, d) + + async_add_entities(new_entities) + if not generate_energy_meters: + return + # Creating cost sensors based on plan + # This will generate hilo_cost_(low|medium|high) sensors which can be + # referred later in the energy dashboard based on the tarif selected + for tarif, amount in tariff_config.items(): + if amount > 0: + sensor_name = f"Hilo rate {tarif}" + cost_entities.append( + HiloCostSensor(hilo, sensor_name, hq_plan_name, amount) + ) + cost_entities.append(HiloCostSensor(hilo, "Hilo rate current", hq_plan_name)) + async_add_entities(cost_entities) + # This setups the utility_meter platform + await utility_manager.update(async_add_entities) + # This sends the entities to the energy dashboard + await energy_manager.update() + + +class BatterySensor(HiloEntity, SensorEntity): + """Define a Battery sensor entity.""" + + _attr_device_class = SensorDeviceClass.BATTERY + _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device): + self._attr_name = f"{device.name} Battery" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = f"{slugify(device.name)}-battery" + self._attr_unique_id = f"{slugify(device.identifier)}-battery" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up BatterySensor entity: {self._attr_name}") + + @property + def state(self): + return str(int(self._device.get_value("battery", 0))) + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + level = round(int(self._device.get_value("battery", 0)) / 10) * 10 + if level < 10: + return "mdi:battery-alert" + return f"mdi:battery-{level}" + + +class Co2Sensor(HiloEntity, SensorEntity): + """Define a Co2 sensor entity.""" + + _attr_device_class = SensorDeviceClass.CO2 + _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device): + self._attr_name = f"{device.name} CO2" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = f"{slugify(device.name)}-co2" + self._attr_unique_id = f"{slugify(device.identifier)}-co2" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up CO2Sensor entity: {self._attr_name}") + + @property + def state(self): + return str(int(self._device.get_value("co2", 0))) + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + return "mdi:molecule-co2" + + +class EnergySensor(IntegrationSensor): + """Define a Hilo energy sensor entity.""" + + _attr_device_class = SensorDeviceClass.ENERGY + _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_state_class = SensorStateClass.TOTAL_INCREASING + _attr_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + + def __init__(self, hilo, device): + self._device = device + self._attr_name = f"{device.name} Hilo Energy" + old_unique_id = f"hilo_energy_{slugify(device.name)}" + self._attr_unique_id = f"{slugify(device.identifier)}-energy" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + self._unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + self._unit_prefix = None + + if device.type == "Meter": + self._attr_name = HILO_ENERGY_TOTAL + self._source = f"sensor.{slugify(device.name)}_power" + # ic-dev21: Set initial state and last_valid_state, removes log errors and unavailable states + initial_state = 0 + self._attr_native_value = initial_state + self._attr_last_valid_state = initial_state + self._device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.identifier)}, + ) + + super().__init__( + integration_method=METHOD_LEFT, + name=self._attr_name, + round_digits=2, + source_entity=self._source, + unique_id=self._attr_unique_id, + unit_prefix="k", + unit_time="h", + device_info=self._device_info, + ) + self._attr_icon = "mdi:lightning-bolt" + LOG.debug( + f"Setting up EnergySensor entity: {self._attr_name} with source {self._source}" + ) + + @property + def unit_of_measurement(self): + return self._attr_unit_of_measurement + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + LOG.debug(f"Added to hass: {self._attr_name}") + await super().async_added_to_hass() + + +class NoiseSensor(HiloEntity, SensorEntity): + """Define a Netatmo noise sensor entity.""" + + _attr_native_unit_of_measurement = UnitOfSoundPressure.DECIBEL + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device): + self._attr_name = f"{device.name} Noise" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = f"{slugify(device.name)}-noise" + self._attr_unique_id = f"{slugify(device.identifier)}-noise" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up NoiseSensor entity: {self._attr_name}") + + @property + def state(self): + return str(int(self._device.get_value("noise", 0))) + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + if int(self._device.get_value("noise", 0)) > 0: + return "mdi:volume-vibrate" + return "mdi:volume-mute" + + +class PowerSensor(HiloEntity, SensorEntity): + """Define a Hilo power sensor entity.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo: Hilo, device: HiloDevice) -> None: + """Initialize.""" + self._attr_name = f"{device.name} Power" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = f"{slugify(device.name)}-power" + self._attr_unique_id = f"{slugify(device.identifier)}-power" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up PowerSensor entity: {self._attr_name}") + + @property + def state(self): + return str(int(self._device.get_value("power", 0))) + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + power = int(self._device.get_value("power", 0)) + if power > 0: + return "mdi:power-plug" + return "mdi:power-plug-off" + + +class TemperatureSensor(HiloEntity, SensorEntity): + """Define a Hilo temperature sensor entity.""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device): + self._attr_name = f"{device.name} Temperature" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = f"{slugify(device.name)}-temperature" + self._attr_unique_id = f"{slugify(device.identifier)}-temperature" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up TemperatureSensor entity: {self._attr_name}") + + @property + def state(self): + return str(float(self._device.get_value("current_temperature", 0))) + + @property + def icon(self): + current_temperature = int(self._device.get_value("current_temperature", 0)) + if not self._device.available: + thermometer = "off" + elif current_temperature >= 22: + thermometer = "high" + elif current_temperature >= 18: + thermometer = "low" + else: + thermometer = "alert" + return f"mdi:thermometer-{thermometer}" + + +class TargetTemperatureSensor(HiloEntity, SensorEntity): + """Define a Hilo target temperature sensor entity.""" + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device): + self._attr_name = f"{device.name} Target Temperature" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = f"{slugify(device.name)}-target-temperature" + self._attr_unique_id = f"{slugify(device.identifier)}-target-temperature" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up TargetTemperatureSensor entity: {self._attr_name}") + + @property + def state(self): + return str(float(self._device.get_value("target_temperature", 0))) + + @property + def icon(self): + target_temperature = int(self._device.get_value("target_temperature", 0)) + if not self._device.available: + thermometer = "off" + elif target_temperature >= 22: + thermometer = "high" + elif target_temperature >= 18: + thermometer = "low" + else: + thermometer = "alert" + return f"mdi:thermometer-{thermometer}" + + +class WifiStrengthSensor(HiloEntity, SensorEntity): + """Define a Wi-Fi strength sensor entity.""" + + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device): + self._attr_name = f"{device.name} WifiStrength" + super().__init__(hilo, name=self._attr_name, device=device) + self._attr_unique_id = f"{slugify(device.name)}-wifistrength" + LOG.debug(f"Setting up WifiStrengthSensor entity: {self._attr_name}") + + @property + def state(self): + return process_wifi(self._device.get_value("wifi_status", 0)) + + @property + def icon(self): + if not self._device.available or self._device.get_value("wifi_status", 0) == 0: + return "mdi:wifi-strength-off" + return f"mdi:wifi-strength-{WIFI_STRENGTH[self.state]}" + + @property + def extra_state_attributes(self): + return {"wifi_signal": self._device.get_value("wifi_status", 0)} + + +class HiloNotificationSensor(HiloEntity, RestoreEntity, SensorEntity): + """Hilo Notification sensor. + Its state will be the number of notification waiting in the Hilo app. + Notifications only used for OneLink's alerts & Low-battery warnings. + We should consider having this sensor enabled only if a smoke detector is in use. + """ + + def __init__(self, hilo, device, scan_interval): + self._attr_name = "Notifications Hilo" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = slugify(self._attr_name) + self._attr_unique_id = ( + f"{slugify(device.identifier)}-{slugify(self._attr_name)}" + ) + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up NotificationSensor entity: {self._attr_name}") + self.scan_interval = timedelta(seconds=NOTIFICATION_SCAN_INTERVAL) + self._state = 0 + self._notifications = [] + self.async_update = Throttle(self.scan_interval)(self._async_update) + + @property + def state(self): + try: + return int(self._state) + except ValueError: + return 0 + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + if self.state > 0: + return "mdi:bell-alert" + return "mdi:bell-outline" + + @property + def should_poll(self): + return True + + @property + def extra_state_attributes(self): + return {"notifications": self._notifications} + + async def async_added_to_hass(self): + """Handle entity about to be added to hass event.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state: + self._last_update = dt_util.utcnow() + self._state = last_state.state + + async def _async_update(self): + self._notifications = [] + for notification in await self._hilo._api.get_event_notifications( + self._hilo.devices.location_id + ): + if notification.get("viewed"): + continue + self._notifications.append( + { + "type_id": notification.get("eventTypeId"), + "event_id": notification.get("eventId"), + "device_id": notification.get("deviceId"), + "date": from_utc_timestamp(notification.get("notificationDateUTC")), + "title": notification.get("notificationTitle"), + "body": notification.get("notificationBody"), + } + ) + self._state = len(self._notifications) + + +class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity): + """Hilo Reward sensor. + Its state will be either the total amount rewarded this season. + """ + + _attr_device_class = SensorDeviceClass.MONETARY + _attr_state_class = SensorStateClass.TOTAL_INCREASING + _entity_component_unrecorded_attributes = frozenset({"history"}) + + def __init__(self, hilo, device, scan_interval): + self._attr_name = "Recompenses Hilo" + + # Check if currency is configured, set a default if not + currency = hilo._hass.config.currency + if currency: + self._attr_native_unit_of_measurement = currency + else: + # Set a default currency or handle the case where currency is not configured + self._attr_native_unit_of_measurement = "CAD" + + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = slugify(self._attr_name) + self._attr_unique_id = ( + f"{slugify(device.identifier)}-{slugify(self._attr_name)}" + ) + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up RewardSensor entity: {self._attr_name}") + self._history_state_yaml: str = "hilo_eventhistory_state.yaml" + self.scan_interval = timedelta(seconds=REWARD_SCAN_INTERVAL) + self._state = 0 + self._history = self._load_history() + self.async_update = Throttle(self.scan_interval)(self._async_update) + + @property + def state(self): + return self._state + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + return "mdi:cash-plus" + + @property + def should_poll(self): + return True + + @property + def extra_state_attributes(self): + return {"history": self._history} + + async def async_added_to_hass(self): + """Handle entity about to be added to hass event.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state: + self._last_update = dt_util.utcnow() + self._state = last_state.state + + async def _async_update(self): + seasons = await self._hilo._api.get_seasons(self._hilo.devices.location_id) + if seasons: + current_history = self._history + new_history = [] + + for idx, season in enumerate(seasons): + current_history_season = next( + ( + item + for item in current_history + if item.get("season") == season.get("season") + ), + None, + ) + + if idx == 0: + self._state = season.get("totalReward", 0) + events = [] + for raw_event in season.get("events", []): + current_history_event = None + event = None + + if current_history_season: + current_history_event = next( + ( + ev + for ev in current_history_season["events"] + if ev["event_id"] == raw_event["id"] + ), + None, + ) + + start_date_utc = datetime.fromisoformat(raw_event["startDateUtc"]) + event_age = datetime.now(timezone.utc) - start_date_utc + if ( + current_history_event + and current_history_event.get("state") == "completed" + and event_age > timedelta(days=1) + ): + # No point updating events for previously completed events, they won't change. + event = current_history_event + else: + details = await self._hilo.get_event_details(raw_event["id"]) + event = Event(**details).as_dict() + + events.append(event) + season["events"] = events + new_history.append(season) + self._history = new_history + self._save_history(new_history) + + def _load_history(self) -> list: + history: list = [] + if isfile(self._history_state_yaml): + with open(self._history_state_yaml) as yaml_file: + LOG.debug("Loading history state from yaml") + history = yaml.load(yaml_file, Loader=yaml.Loader) + return history + + def _save_history(self, history: list): + with open(self._history_state_yaml, "w") as yaml_file: + LOG.debug("Saving history state to yaml file") + yaml.dump(history, yaml_file, Dumper=yaml.RoundTripDumper) + + +class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity): + """Hilo challenge sensor. + Its state will be either: + - off: no ongoing or scheduled challenge + - scheduled: A challenge is scheduled, details in the next_events + extra attribute + - pre_cold: optional phase to cool further before appreciation + - appreciation: optional phase to pre-heat more before challenge + - pre_heat: Currently in the pre-heat phase + - reduction or on: Challenge is currently active, heat is lowered + - recovery: Challenge is completed, we're reheating. + """ + + def __init__(self, hilo, device, scan_interval): + self._attr_name = "Defi Hilo" + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = slugify(self._attr_name) + self._attr_unique_id = ( + f"{slugify(device.identifier)}-{slugify(self._attr_name)}" + ) + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up ChallengeSensor entity: {self._attr_name}") + # note ic-dev21: scan time at 5 minutes (300s) will force local update + self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL_REDUCTION) + self._state = "off" + self._next_events = [] + self.async_update = Throttle(self.scan_interval)(self._async_update) + + @property + def state(self): + if len(self._next_events) > 0: + event = Event(**{**{"id": 0}, **self._next_events[0]}) + return event.state + else: + return "off" + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + if self.state == "appreciation": + return "mdi:glass-cocktail" + if self.state == "off": + return "mdi:lightning-bolt" + if self.state == "scheduled": + return "mdi:progress-clock" + if self.state == "pre_heat": + return "mdi:radiator" + if self.state in ["reduction", "on"]: + return "mdi:power-plug-off" + if self.state == "recovery": + return "mdi:calendar-check" + if self.state == "pre_cold": + return "mdi:radiator-off" + return "mdi:battery-alert" + + @property + def should_poll(self): + return True + + @property + def extra_state_attributes(self): + return {"next_events": self._next_events} + + async def async_added_to_hass(self): + """Handle entity about to be added to hass event.""" + await super().async_added_to_hass() + last_state = await self.async_get_last_state() + if last_state: + self._last_update = dt_util.utcnow() + self._state = last_state.state + self._next_events = last_state.attributes.get("next_events", []) + + async def _async_update(self): + self._next_events = [] + events = await self._hilo._api.get_gd_events(self._hilo.devices.location_id) + LOG.debug(f"Events received from Hilo: {events}") + for raw_event in events: + details = await self._hilo.get_event_details(raw_event["id"]) + event = Event(**details) + if self._hilo.appreciation > 0: + event.appreciation(self._hilo.appreciation) + if self._hilo.pre_cold > 0: + event.pre_cold(self._hilo.pre_cold) + self._next_events.append(event.as_dict()) + + +class DeviceSensor(HiloEntity, SensorEntity): + """Devices like the gateway or Smoke Detectors don't have many attributes, + except for the "disconnected" attribute. These entities are monitoring + this state. + """ + + def __init__(self, hilo, device): + self._attr_name = device.name + super().__init__(hilo, name=self._attr_name, device=device) + old_unique_id = slugify(device.name) + self._attr_unique_id = f"{slugify(device.identifier)}-{slugify(device.name)}" + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + LOG.debug(f"Setting up DeviceSensor entity: {self._attr_name}") + + @property + def state(self): + return "on" if self._device.available else "off" + + @property + def extra_state_attributes(self): + return {k: self._device.get_value(k) for k in self._device.attributes} + + @property + def icon(self): + if not self._device.available: + return "mdi:lan-disconnect" + if self.state == "off": + return "mdi:access-point-network-off" + return "mdi:access-point-network" + + +class HiloCostSensor(HiloEntity, RestoreEntity, SensorEntity): + _attr_device_class = SensorDeviceClass.MONETARY + _attr_native_unit_of_measurement = ( + f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" + ) + _attr_state_class = SensorStateClass.MEASUREMENT + _attr_icon = "mdi:cash" + + def __init__(self, hilo, name, plan_name, amount=0): + for d in hilo.devices.all: + if d.type == "Gateway": + device = d + if "low_threshold" in name: + self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + self.data = None + self._attr_name = name + self.plan_name = plan_name + self._amount = amount + old_unique_id = slugify(self._attr_name) + self._attr_unique_id = ( + f"{slugify(device.identifier)}-{slugify(self._attr_name)}" + ) + hilo.async_migrate_unique_id( + old_unique_id, self._attr_unique_id, Platform.SENSOR + ) + self._last_update = dt_util.utcnow() + super().__init__(hilo, name=self._attr_name, device=device) + LOG.info(f"Initializing energy cost sensor {name} {plan_name} Amount: {amount}") + + @property + def state(self): + return self._amount + + @property + def should_poll(self) -> bool: + return False + + @property + def extra_state_attributes(self): + return {"last_update": self._last_update, "Cost": self.state} + + async def async_added_to_hass(self): + """Handle entity about to be added to hass event.""" + await super().async_added_to_hass() + + async def async_update(self): + return + + +# class HiloOutdoorTempSensor(HiloEntity, RestoreEntity, SensorEntity): +# je ne crois pas qu'on a besoin d'un restoreentity pour une température. +# La dernière valeur n'a pas vraiment d'importance? +class HiloOutdoorTempSensor(HiloEntity, SensorEntity): + """Hilo outdoor temperature sensor. + Its state will be the current outdoor weather as reported by the Hilo App + """ + + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hilo, device, scan_interval): + self._attr_name = "Outdoor Weather Hilo" + super().__init__(hilo, name=self._attr_name, device=device) + # old_unique_id = slugify(self._attr_name) + # pas requis pusisqu'on l'a jamais créé avec un autre uniqueID + # par contre on peut laisser pour être comme les autres sensors + self._attr_unique_id = ( + f"{slugify(device.identifier)}-{slugify(self._attr_name)}" + ) + # hilo.async_migrate_unique_id( + # old_unique_id, self._attr_unique_id, Platform.SENSOR + # ) + LOG.debug(f"Setting up OutdoorWeatherSensor entity: {self._attr_name}") + self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL_REDUCTION) + self._state = STATE_UNKNOWN + self._weather = {} + self.async_update = Throttle(self.scan_interval)(self._async_update) + + @property + def state(self): + try: + return int(self._state) + except ValueError: + return STATE_UNKNOWN + + @property + def icon(self): + condition = self._weather.get("condition", "").lower() + LOG.warning(f"Current condition: {condition}") + if not condition: + return "mdi:lan-disconnect" + # le code est moins lourd en utilisant une constate, en plus on garde une constante similaire à Hilo + return WEATHER_CONDITIONS.get(self._weather.get("condition", "Unknown")) + + @property + def should_poll(self): + return True + + @property + def extra_state_attributes(self): + LOG.debug(f"Adding weather {self._weather}") + # Les attributes n'avait pas l'aire créé séparément mais plutot juste en une seule string + # J'ai enlevé temperature puis qu'elle était difini 2 fois dans le fond. + # J'ai enlevé icon puisque c'était 0 et je voulais pas que ça rentre en conflis avec celle de HA + return { + key: self._weather[key] + for key in self._weather + if key not in ["temperature", "icon"] + } + + # async def async_added_to_hass(self): + # """Handle entity about to be added to hass event.""" + # await super().async_added_to_hass() + # last_state = await self.async_get_last_state() + # if last_state: + # self._last_update = dt_util.utcnow() + # self._state = last_state.state + + async def _async_update(self): + self._weather = {} + self._weather = await self._hilo._api.get_weather( + self._hilo.devices.location_id + ) + self._state = self._weather.get("temperature") From 3a88447fc9466e4c1029e7979f2ff73c542f4928 Mon Sep 17 00:00:00 2001 From: maxyvon Date: Tue, 16 Apr 2024 16:02:46 -0400 Subject: [PATCH 5/5] add spaces to WeatherConditions --- custom_components/hilo/const.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/custom_components/hilo/const.py b/custom_components/hilo/const.py index 42ba0e8..7f3518a 100755 --- a/custom_components/hilo/const.py +++ b/custom_components/hilo/const.py @@ -73,22 +73,22 @@ WEATHER_CONDITIONS = { "Unknown": "mdi:weather-sunny-alert", - "BlowingSnow": "mdi:weather-snowy-heavy", + "Blowing Snow": "mdi:weather-snowy-heavy", "Clear": "mdi:weather-sunny", "Cloudy": "mdi:weather-cloudy", "Fair": "mdi:weather-partly-cloudy", "Foggy": "mdi:weather-fog", - "HailSleet": "mdi:weather-hail", - "MostlyCloudy": "mdi:weather-partly-cloudy", + "Hail Sleet": "mdi:weather-hail", + "Mostly Cloudy": "mdi:weather-partly-cloudy", "Rain": "mdi:weather-rainy", - "RainSnow": "mdi:weather-snowy-rainy", + "Rain Snow": "mdi:weather-snowy-rainy", "Snow": "mdi:weather-snowy", "Thunder": "mdi:weather-lightning", "Windy": "mdi:weather-windy", } # Class lists -LIGHT_CLASSES = ["LightDimmer", "WhiteBulb", "ColorBulb"] +LIGHT_CLASSES = ["LightDimmer", "WhiteBulb", "ColorBulb", "LightSwitch"] HILO_SENSOR_CLASSES = [ "SmokeDetector", "IndoorWeatherStation", @@ -96,4 +96,4 @@ "Gateway", ] CLIMATE_CLASSES = ["Thermostat", "FloorThermostat", "Thermostat24V"] -SWITCH_CLASSES = ["LightSwitch", "Outlet", "Ccr", "Cee"] +SWITCH_CLASSES = ["Outlet", "Ccr", "Cee"]