From 385a55bd05c6b39a6e6c17d580b601e436aca57a Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 11:49:21 -0500 Subject: [PATCH 01/14] Moving powersensor close to energysensor --- custom_components/hilo/sensor.py | 56 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 48731a9..fdbc669 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -235,6 +235,34 @@ def icon(self): return "mdi:molecule-co2" +class PowerSensor(HiloEntity, SensorEntity): + """Define a Hilo power sensor entity.""" + + _attr_device_class = SensorDeviceClass.POWER + _attr_native_unit_of_measurement = POWER_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) + self._attr_unique_id = f"{slugify(device.name)}-power" + 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 EnergySensor(IntegrationSensor): """Define a Hilo energy sensor entity.""" @@ -308,34 +336,6 @@ def icon(self): return "mdi:volume-mute" -class PowerSensor(HiloEntity, SensorEntity): - """Define a Hilo power sensor entity.""" - - _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = POWER_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) - self._attr_unique_id = f"{slugify(device.name)}-power" - 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.""" From a799578be0f274b7fa08aa7d9829a9b865c8dc6d Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 12:56:33 -0500 Subject: [PATCH 02/14] Adding back w/h default unit for energy sensor --- custom_components/hilo/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index fdbc669..c833c3e 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -15,6 +15,7 @@ CONF_SCAN_INTERVAL, CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -275,7 +276,7 @@ def __init__(self, device): self._device = device self._attr_name = f"Hilo Energy {slugify(device.name)}" self._attr_unique_id = f"hilo_energy_{slugify(device.name)}" - self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._unit_of_measurement = ENERGY_WATT_HOUR self._unit_prefix = None if device.type == "Meter": self._attr_name = HILO_ENERGY_TOTAL From 1d6c26406a22b43d17698998209c8d8c38aa4ce9 Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 13:39:10 -0500 Subject: [PATCH 03/14] Adding a lot more log --- custom_components/hilo/sensor.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index c833c3e..b8f6c9c 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -22,7 +22,7 @@ SOUND_PRESSURE_DB, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import Throttle, slugify @@ -308,8 +308,32 @@ def unit_of_measurement(self): async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" - LOG.debug(f"Added to hass: {self._attr_name}") + LOG.debug( + f"async_added_to_hass(): Adding to hass: {self._attr_name=} {self._attr_native_value=} {self._unit_of_measurement=} {self._last_valid_state=} {self._state=} {self._attr_device_class=}" + ) await super().async_added_to_hass() + LOG.debug( + f"async_added_to_hass(): Adding to hass: {self._attr_name=} {self._attr_native_value=} {self._unit_of_measurement=} {self._last_valid_state=} {self._state=} {self._attr_device_class=}" + ) + + async def async_get_last_sensor_data(self): + last_sensor_data = await super().async_get_last_sensor_data() + LOG.debug(f"async_get_last_sensor_data(): {last_sensor_data=}") + if last_sensor_data: + LOG.debug( + f"async_get_last_sensor_data(): {last_sensor_data.native_value=} {last_sensor_data.last_valid_state=}" + ) + return last_sensor_data + + async def async_get_last_state(self): + last_state = await super().async_get_last_state() + LOG.debug(f"async_get_last_state(): {last_state=}") + return last_state + + @callback + def calc_integration(event): + LOG.debug(f"calc_integration(): {event=}") + super().calc_integration(event) class NoiseSensor(HiloEntity, SensorEntity): From 82677d3427c34c4b9745e924e635ed937f8dad56 Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 14:18:50 -0500 Subject: [PATCH 04/14] Adding more logging and automating dashboard provisionning --- custom_components/hilo/managers.py | 1 + custom_components/hilo/sensor.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/custom_components/hilo/managers.py b/custom_components/hilo/managers.py index 8dee8c9..e691c7b 100644 --- a/custom_components/hilo/managers.py +++ b/custom_components/hilo/managers.py @@ -80,6 +80,7 @@ def __init__(self): @property def msg(self): + LOG.debug(f"Updating dashboard with {self.src=} {self.dev=}") return { "energy_sources": self.src, "device_consumption": self.dev, diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index b8f6c9c..da6e171 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -155,7 +155,11 @@ def create_energy_entity(device): energy_entity = HILO_ENERGY_TOTAL tariff_list = validate_tariff_list(tariff_config) net_consumption = device.net_consumption + LOG.debug( + f"create_energy_entity(): {energy_entity=} {tariff_list=} {net_consumption=}" + ) utility_manager.add_meter(energy_entity, tariff_list, net_consumption) + energy_manager.add_to_dashboard(energy_entity, tariff_list) for d in hilo.devices.all: LOG.debug(f"Adding device {d}") From a472c1c6e03a04e5754ac709da2644bdff910280 Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 15:43:33 -0500 Subject: [PATCH 05/14] Filling missing conf_tariffs for existing meters --- custom_components/hilo/managers.py | 55 ++++++++++++++++++------------ 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/custom_components/hilo/managers.py b/custom_components/hilo/managers.py index e691c7b..4c921ac 100644 --- a/custom_components/hilo/managers.py +++ b/custom_components/hilo/managers.py @@ -3,7 +3,20 @@ from homeassistant.components.energy.data import async_get_manager from homeassistant.components.utility_meter import async_setup as utility_setup -from homeassistant.components.utility_meter.const import DOMAIN as UTILITY_DOMAIN +from homeassistant.components.utility_meter.const import ( + CONF_METER, + CONF_METER_DELTA_VALUES, + CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_PERIODICALLY_RESETTING, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, + CONF_TARIFF, + CONF_TARIFFS, + DATA_TARIFF_SENSORS, + DATA_UTILITY, + DOMAIN as UTILITY_DOMAIN, +) from homeassistant.components.utility_meter.sensor import ( async_setup_platform as utility_setup_platform, ) @@ -26,7 +39,7 @@ def add_meter(self, entity, tariff_list, net_consumption=False): self.add_meter_config(entity, tariff_list, net_consumption) def add_meter_entity(self, entity, tariff_list): - if entity in self.hass.data.get("utility_meter_data", {}): + if entity in self.hass.data.get(DATA_UTILITY, {}): LOG.debug(f"Entity {entity} is already in the utility meters") return self.new_entities += 1 @@ -35,9 +48,9 @@ def add_meter_entity(self, entity, tariff_list): meter_name = f"{name} {tarif}" LOG.debug(f"Creating UtilityMeter entity for {entity}: {meter_name}") self.meter_entities[meter_name] = { - "meter": entity, + CONF_METER: entity, "name": meter_name, - "tariff": tarif, + CONF_TARIFF: tarif, } def add_meter_config(self, entity, tariff_list, net_consumption): @@ -45,29 +58,29 @@ def add_meter_config(self, entity, tariff_list, net_consumption): LOG.debug( f"Creating UtilityMeter config: {name} {tariff_list} (Net Consumption: {net_consumption})" ) - self.meter_configs[entity] = OrderedDict( - { - "source": f"sensor.{entity}", - "name": name, - "cycle": self.period, - "tariffs": tariff_list, - "net_consumption": net_consumption, - "utility_meter_sensors": [], - "offset": timedelta(0), - "delta_values": False, - "periodically_resetting": True, - } - ) + self.meter_configs[entity] = { + CONF_SOURCE_SENSOR: f"sensor.{entity}", + "name": name, + CONF_METER_TYPE: self.period, + CONF_TARIFFS: tariff_list, + CONF_METER_NET_CONSUMPTION: net_consumption, + DATA_TARIFF_SENSORS: [], + CONF_METER_OFFSET: timedelta(0), + CONF_METER_DELTA_VALUES: False, + CONF_METER_PERIODICALLY_RESETTING: True, + } async def update(self, async_add_entities): LOG.debug(f"Setting up UtilityMeter entities {UTILITY_DOMAIN}") if self.new_entities == 0: LOG.debug("No new entities, not setting up again") return - config = {} - config[UTILITY_DOMAIN] = OrderedDict( - {**self.hass.data.get("utility_meter_data", {}), **self.meter_configs} - ) + current_meters = self.hass.data.get(DATA_UTILITY, {}) + for meter, conf in current_meters.items(): + if CONF_TARIFFS not in conf: + conf[CONF_TARIFFS] = [] + config = {UTILITY_DOMAIN: OrderedDict({**current_meters, **self.meter_configs})} + LOG.debug(f"Config passed to utility_meters: {config=}") await utility_setup(self.hass, config) await utility_setup_platform( self.hass, config, async_add_entities, self.meter_entities From 0ba3199c6d5367871c3778d5e21bca1e10046933 Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 18:11:47 -0500 Subject: [PATCH 06/14] Fixing energy meters --- custom_components/hilo/managers.py | 24 +++++++++++++++--------- custom_components/hilo/sensor.py | 4 +++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/custom_components/hilo/managers.py b/custom_components/hilo/managers.py index 4c921ac..b5f0d2a 100644 --- a/custom_components/hilo/managers.py +++ b/custom_components/hilo/managers.py @@ -75,12 +75,17 @@ async def update(self, async_add_entities): if self.new_entities == 0: LOG.debug("No new entities, not setting up again") return - current_meters = self.hass.data.get(DATA_UTILITY, {}) - for meter, conf in current_meters.items(): - if CONF_TARIFFS not in conf: - conf[CONF_TARIFFS] = [] - config = {UTILITY_DOMAIN: OrderedDict({**current_meters, **self.meter_configs})} - LOG.debug(f"Config passed to utility_meters: {config=}") + # NOTE(dvd): Pas sur si c'est relevant de pousser les config originale + # current_meters = self.hass.data.get(DATA_UTILITY, {}) + # for meter, conf in current_meters.items(): + # conf[CONF_TARIFFS] = conf.get(CONF_TARIFFS, []) + # conf[CONF_METER_OFFSET] = conf.get(CONF_METER_OFFSET, timedelta(0)) + # conf[CONF_METER_DELTA_VALUES] = conf.get(CONF_METER_DELTA_VALUES, False) + # conf[CONF_METER_TYPE] = conf.get(CONF_METER_TYPE, "daily") + # conf[CONF_METER_NET_CONSUMPTION] = conf.get(CONF_METER_NET_CONSUMPTION, True) + # conf[CONF_METER_PERIODICALLY_RESETTING] = conf.get(CONF_METER_PERIODICALLY_RESETTING, True) + config = {UTILITY_DOMAIN: OrderedDict(self.meter_configs)} + LOG.debug(f"Performing utility_setup: {config=}") await utility_setup(self.hass, config) await utility_setup_platform( self.hass, config, async_add_entities, self.meter_entities @@ -131,20 +136,21 @@ def add_flow_from(self, sensor, rate): "entity_energy_price": f"sensor.{rate}", "number_energy_price": None, } - LOG.debug(f"Adding {sensor} / {rate} to grid source") + LOG.debug(f"Adding from flow: {sensor} / {rate} to grid source {flow=}") self.src[0]["flow_from"].append(flow) def add_device(self, sensor): sensor = f"sensor.{sensor}" + LOG.debug(f"energy dashboard: Adding {sensor} to individual device consumption") if any(d["stat_consumption"] == sensor for d in self.dev): return - LOG.debug(f"Adding {sensor} to individual device consumption") self.updated = True self.dev.append({"stat_consumption": sensor}) + LOG.debug(f"energy dashboard: Added {sensor} to individual device consumption") def add_to_dashboard(self, entity, tariff_list): for tarif in tariff_list: - name = f"{entity}_{self.period}" + name = f"{entity}" if entity == HILO_ENERGY_TOTAL: self.add_flow_from(f"{name}_{tarif}", f"hilo_rate_{tarif}") else: diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index da6e171..64ad5b6 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -272,7 +272,7 @@ class EnergySensor(IntegrationSensor): """Define a Hilo energy sensor entity.""" _attr_device_class = SensorDeviceClass.ENERGY - _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_icon = "mdi:lightning-bolt" @@ -319,6 +319,8 @@ async def async_added_to_hass(self) -> None: LOG.debug( f"async_added_to_hass(): Adding to hass: {self._attr_name=} {self._attr_native_value=} {self._unit_of_measurement=} {self._last_valid_state=} {self._state=} {self._attr_device_class=}" ) + if state := await self.async_get_last_state(): + self._state = state.state async def async_get_last_sensor_data(self): last_sensor_data = await super().async_get_last_sensor_data() From eaba974fcea2f2905d7685442d9297a307db28e2 Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Mon, 4 Dec 2023 19:02:17 -0500 Subject: [PATCH 07/14] Reverting back to kWh --- custom_components/hilo/sensor.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 64ad5b6..c89fff9 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -15,7 +15,6 @@ CONF_SCAN_INTERVAL, CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -272,7 +271,7 @@ class EnergySensor(IntegrationSensor): """Define a Hilo energy sensor entity.""" _attr_device_class = SensorDeviceClass.ENERGY - _attr_native_unit_of_measurement = ENERGY_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL_INCREASING _attr_icon = "mdi:lightning-bolt" @@ -280,15 +279,10 @@ def __init__(self, device): self._device = device self._attr_name = f"Hilo Energy {slugify(device.name)}" self._attr_unique_id = f"hilo_energy_{slugify(device.name)}" - self._unit_of_measurement = ENERGY_WATT_HOUR - self._unit_prefix = None + self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._unit_prefix = "k" if device.type == "Meter": self._attr_name = HILO_ENERGY_TOTAL - self._unit_of_measurement = ENERGY_KILO_WATT_HOUR - self._unit_prefix = "k" - if device.type == "Thermostat" or device.type == "FloorThermostat": - self._unit_of_measurement = ENERGY_KILO_WATT_HOUR - self._unit_prefix = "k" self._source = f"sensor.{slugify(device.name)}_power" super().__init__( From 6a05f4c0800d8633d84f31e582515825ec177d37 Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Wed, 6 Dec 2023 23:38:41 -0500 Subject: [PATCH 08/14] Changing the way we keep track of state for CostSensor Apparently, there's a race condition in hass where, when we set the state for a static entity, by the time the state is updated, it's reverted back to the original state. This causes the entity state to bounce back and forth. When we control what exactly what can actually update the state by listening to events and only setting it under specific condition, we get stable results. --- custom_components/hilo/__init__.py | 39 +++++---- custom_components/hilo/sensor.py | 130 ++++++++++++++++++----------- 2 files changed, 107 insertions(+), 62 deletions(-) diff --git a/custom_components/hilo/__init__.py b/custom_components/hilo/__init__.py index 92ce0fa..0ad44ac 100755 --- a/custom_components/hilo/__init__.py +++ b/custom_components/hilo/__init__.py @@ -462,7 +462,7 @@ async def async_update(self) -> None: self.check_tarif() def set_state(self, entity, state, new_attrs={}, keep_state=False, force=False): - params = f"entity={entity}, state={state}, new_attrs={new_attrs}, keep_state={keep_state}" + params = f"{entity=} {state=} {new_attrs=} {keep_state=}" current = self._hass.states.get(entity) if not current: if not force: @@ -471,14 +471,15 @@ def set_state(self, entity, state, new_attrs={}, keep_state=False, force=False): attrs = {} else: attrs = dict(current.as_dict()["attributes"]) - LOG.debug(f"Setting state {params} {current}") attrs["last_update"] = datetime.now() + attrs["hilo_update"] = True attrs = {**attrs, **new_attrs} if keep_state and current: state = current.state if "Cost" in attrs: attrs["Cost"] = state - self._hass.states.async_set(entity, state, attrs) + LOG.debug(f"Setting state {params} {current=} {attrs=}") + self._hass.states.async_set(entity, state, attrs, force_update=force) @property def high_times(self): @@ -490,32 +491,35 @@ def high_times(self): def check_tarif(self): if self.generate_energy_meters: tarif = "low" - base_sensor = f"sensor.{HILO_ENERGY_TOTAL}_low" + base_sensor = f"sensor.{HILO_ENERGY_TOTAL}" energy_used = self._hass.states.get(base_sensor) if not energy_used: - LOG.warning(f"check_tarif: Unable to find state for {base_sensor}") + LOG.warning(f"check_tarif(): Unable to find state for {base_sensor}") return tarif plan_name = self.hq_plan_name tarif_config = CONF_TARIFF.get(plan_name) current_cost = self._hass.states.get("sensor.hilo_rate_current") + current_state = 0 if not current_cost else current_cost.state try: if float(energy_used.state) >= tarif_config.get("low_threshold"): tarif = "medium" except ValueError: LOG.warning( - f"Unable to restore a valid state of {base_sensor}: {energy_used.state}" + f"check_tarif(): Unable to restore a valid state of {base_sensor}: {energy_used.state}" ) if tarif_config.get("high", 0) > 0 and self.high_times: tarif = "high" target_cost = self._hass.states.get(f"sensor.hilo_rate_{tarif}") - if target_cost.state != current_cost.state: + if target_cost.state != current_state: LOG.debug( - f"check_tarif: Updating current cost, was {current_cost.state} now {target_cost.state}" + f"check_tarif: Updating current cost, was {current_state=} now {target_cost.state=}" + ) + self.set_state( + "sensor.hilo_rate_current", target_cost.state, force=True ) - self.set_state("sensor.hilo_rate_current", target_cost.state) LOG.debug( - f"check_tarif: Current plan: {plan_name} Target Tarif: {tarif} Energy used: {energy_used.state} Peak: {self.high_times}" + f"check_tarif: Current plan: {plan_name} Target Tarif: {tarif} Energy used: {energy_used.state} Peak: {self.high_times} {target_cost.state=} {current_state=}" ) known_power = 0 smart_meter = "sensor.smartenergymeter_power" @@ -560,15 +564,20 @@ def check_tarif(self): @callback def fix_utility_sensor(self, entity, state): - """not sure why this doesn't get created with a proper device_class""" + """For some reason, the utility sensors are missing their + measurement units and device classes.""" current_state = state.as_dict() attrs = current_state.get("attributes", {}) if not attrs.get("source"): - LOG.debug(f"No source entity defined on {entity}: {current_state}") + LOG.debug( + f"fix_utility_sensor(): No source entity defined on {entity}: {current_state}" + ) return parent_unit = self._hass.states.get(attrs.get("source")) if not parent_unit: - LOG.warning(f"Unable to find state for parent unit: {current_state}") + LOG.warning( + f"fix_utility_sensor(): Unable to find state for parent unit: {current_state}" + ) return new_attrs = { ATTR_UNIT_OF_MEASUREMENT: parent_unit.as_dict() @@ -578,7 +587,7 @@ def fix_utility_sensor(self, entity, state): } if not all(a in attrs.keys() for a in new_attrs.keys()): LOG.warning( - f"Fixing utility sensor: {entity} {current_state} new_attrs: {new_attrs}" + f"fix_utility_sensor(): Fixing utility sensor: {entity=} {current_state=} new_attrs: {new_attrs=}" ) self.set_state(entity, None, new_attrs=new_attrs, keep_state=True) @@ -588,7 +597,7 @@ def set_tarif(self, entity, current, new): return if entity.startswith("select.hilo_energy") and current != new: LOG.debug( - f"check_tarif: Changing tarif of {entity} from {current} to {new}" + f"set_tarif(): Changing tarif of {entity=} from {current=} to {new=}" ) context = Context() data = {ATTR_OPTION: new, "entity_id": entity} diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index c89fff9..5f22c58 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -23,6 +23,7 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import Throttle, slugify import homeassistant.util.dt as dt_util @@ -180,8 +181,14 @@ def create_energy_entity(device): cost_entities.append( HiloCostSensor(hilo, sensor_name, hq_plan_name, amount) ) - cost_entities.append(HiloCostSensor(hilo, "Hilo rate current", hq_plan_name)) + hilo_rate_current = HiloCostSensor(hilo, "Hilo rate current", hq_plan_name) + cost_entities.append(hilo_rate_current) async_add_entities(cost_entities) + # hilo._hass.bus.async_listen(EVENT_STATE_CHANGED, hilo_rate_current._handle_state_change) + async_track_state_change_event( + hilo._hass, ["sensor.hilo_rate_current"], hilo_rate_current._handle_state_change + ) + # This setups the utility_meter platform await utility_manager.update(async_add_entities) # This sends the entities to the energy dashboard @@ -566,36 +573,52 @@ async def async_added_to_hass(self): 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 not seasons: + return + 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, - ) + 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: + # Either it is an unknown event, one that is still in progress or a recent one, get the details. + details = await self._hilo._api.get_gd_events( + self._hilo.devices.location_id, event_id=raw_event["id"] + ) + event = Event(**details).as_dict() start_date_utc = datetime.fromisoformat(raw_event["startDateUtc"]) event_age = datetime.now(timezone.utc) - start_date_utc @@ -727,43 +750,56 @@ def icon(self): return "mdi:access-point-network" -class HiloCostSensor(HiloEntity, RestoreEntity, SensorEntity): +class HiloCostSensor(HiloEntity, SensorEntity): _attr_device_class = SensorDeviceClass.MONETARY _attr_native_unit_of_measurement = f"{CURRENCY_DOLLAR}/{ENERGY_KILO_WATT_HOUR}" - _attr_state_class = SensorStateClass.MEASUREMENT + _attr_state_class = SensorStateClass.TOTAL _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 + device = next((d for d in hilo.devices.all if d.type == "Gateway"), None) if "low_threshold" in name: self._attr_device_class = SensorDeviceClass.ENERGY self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - self.data = None self._attr_name = name self.plan_name = plan_name - self._amount = amount self._attr_unique_id = slugify(self._attr_name) self._last_update = dt_util.utcnow() + self._cost = amount 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 + def _handle_state_change(self, event): + if (state := event.data.get("new_state")) is None: + return + if state.entity_id != f"sensor.{self._attr_unique_id}": + return + try: + if state.attributes.get("hilo_update"): + LOG.debug( + f"Setting new state {state.state} {state=} {state.attributes=}" + ) + self._cost = state.state + self._last_update = dt_util.utcnow() + except ValueError: + LOG.error(f"Invalidate state received for {self._attr_unique_id}: {state}") @property - def should_poll(self) -> bool: - return False + def state(self): + return self._cost @property def extra_state_attributes(self): - return {"last_update": self._last_update, "Cost": self.state} + return { + "Cost": self._cost, + "Plan": self.plan_name, + "last_update": self._last_update, + } - async def async_added_to_hass(self): - """Handle entity about to be added to hass event.""" - await super().async_added_to_hass() + @property + def should_poll(self) -> bool: + return False async def async_update(self): - return + self._last_update = dt_util.utcnow() + return super().async_update() From d658a1592745c32695832a153487f23767c1b6bc Mon Sep 17 00:00:00 2001 From: David Vallee Delisle Date: Sat, 16 Dec 2023 10:18:06 -0500 Subject: [PATCH 09/14] Skipping events received within 30 seconds of each other --- custom_components/hilo/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 5f22c58..d3b3ab9 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -774,13 +774,17 @@ def _handle_state_change(self, event): return if state.entity_id != f"sensor.{self._attr_unique_id}": return + now = dt_util.utcnow() try: - if state.attributes.get("hilo_update"): + if ( + state.attributes.get("hilo_update") + and self._last_update + timedelta(seconds=30) < now + ): LOG.debug( f"Setting new state {state.state} {state=} {state.attributes=}" ) self._cost = state.state - self._last_update = dt_util.utcnow() + self._last_update = now except ValueError: LOG.error(f"Invalidate state received for {self._attr_unique_id}: {state}") From 45a959dc5c09342af7992f17736710c21271e8c5 Mon Sep 17 00:00:00 2001 From: Ian C <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 28 Mar 2024 19:52:39 -0400 Subject: [PATCH 10/14] Update sensor.py Typo + linting --- custom_components/hilo/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index e56b546..f7cfaa0 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -263,7 +263,7 @@ class PowerSensor(HiloEntity, SensorEntity): """Define a Hilo power sensor entity.""" _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = POWER_WATT + _attr_native_unit_of_measurement = UnitOfPower.Watt _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo: Hilo, device: HiloDevice) -> None: @@ -832,7 +832,9 @@ def icon(self): class HiloCostSensor(HiloEntity, SensorEntity): _attr_device_class = SensorDeviceClass.MONETARY - _attr_native_unit_of_measurement = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" + _attr_native_unit_of_measurement = ( + f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" + ) _attr_state_class = SensorStateClass.TOTAL _attr_icon = "mdi:cash" From 13b7ee84968f24ac9c6d0b602a7330aad393d647 Mon Sep 17 00:00:00 2001 From: Ian C <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 28 Mar 2024 19:58:42 -0400 Subject: [PATCH 11/14] Update sensor.py --- custom_components/hilo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index f7cfaa0..1ae8165 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -263,7 +263,7 @@ class PowerSensor(HiloEntity, SensorEntity): """Define a Hilo power sensor entity.""" _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = UnitOfPower.Watt + _attr_native_unit_of_measurement = UnitOfPower.WATT _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo: Hilo, device: HiloDevice) -> None: From 8b4d41f1ac33f18a09145cf2ccfa0790bff1136b Mon Sep 17 00:00:00 2001 From: Ian C <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:20:04 -0400 Subject: [PATCH 12/14] Revert "Update sensor.py" This reverts commit 13b7ee84968f24ac9c6d0b602a7330aad393d647. --- custom_components/hilo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index 1ae8165..f7cfaa0 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -263,7 +263,7 @@ class PowerSensor(HiloEntity, SensorEntity): """Define a Hilo power sensor entity.""" _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = UnitOfPower.WATT + _attr_native_unit_of_measurement = UnitOfPower.Watt _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo: Hilo, device: HiloDevice) -> None: From 31c11cb7f5c8bf6a30c6083b73a40fd76ea9f5ec Mon Sep 17 00:00:00 2001 From: Ian C <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:20:22 -0400 Subject: [PATCH 13/14] Revert "Update sensor.py" This reverts commit 45a959dc5c09342af7992f17736710c21271e8c5. --- custom_components/hilo/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index f7cfaa0..e56b546 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -263,7 +263,7 @@ class PowerSensor(HiloEntity, SensorEntity): """Define a Hilo power sensor entity.""" _attr_device_class = SensorDeviceClass.POWER - _attr_native_unit_of_measurement = UnitOfPower.Watt + _attr_native_unit_of_measurement = POWER_WATT _attr_state_class = SensorStateClass.MEASUREMENT def __init__(self, hilo: Hilo, device: HiloDevice) -> None: @@ -832,9 +832,7 @@ def icon(self): class HiloCostSensor(HiloEntity, SensorEntity): _attr_device_class = SensorDeviceClass.MONETARY - _attr_native_unit_of_measurement = ( - f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" - ) + _attr_native_unit_of_measurement = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" _attr_state_class = SensorStateClass.TOTAL _attr_icon = "mdi:cash" From d44be2d388a41d0110bd0e7739ed54362d2a1e82 Mon Sep 17 00:00:00 2001 From: Ian C <108159253+ic-dev21@users.noreply.github.com> Date: Thu, 28 Mar 2024 20:20:30 -0400 Subject: [PATCH 14/14] Revert "Merge branch 'main' into fix_attempt_energy" This reverts commit a2458b0650d6affa8159b402c1618c889516ec70, reversing changes made to d658a1592745c32695832a153487f23767c1b6bc. --- .github/FUNDING.yml | 2 - .github/workflows/constraints.txt | 8 +- .github/workflows/labeler.yml | 1 + .github/workflows/release-drafter.yml | 3 +- .github/workflows/tests.yaml | 5 +- .gitignore | 1 - CONTRIBUTING.md | 10 +- README.en.md | 113 +--- README.md | 147 ++---- custom_components/hilo/__init__.py | 233 ++------- custom_components/hilo/climate.py | 15 +- custom_components/hilo/config_flow.py | 207 ++++++-- custom_components/hilo/const.py | 4 - custom_components/hilo/light.py | 20 +- custom_components/hilo/managers.py | 45 +- custom_components/hilo/manifest.json | 4 +- custom_components/hilo/sensor.py | 178 ++----- custom_components/hilo/switch.py | 7 +- custom_components/hilo/translations/en.json | 17 +- custom_components/hilo/translations/fr.json | 21 +- doc/automations/README.md | 539 +------------------- info.md | 8 +- 22 files changed, 360 insertions(+), 1228 deletions(-) delete mode 100644 .github/FUNDING.yml mode change 100755 => 100644 custom_components/hilo/climate.py diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 40a28c4..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ - -custom: https://paypal.me/icdev21 diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt index b464d2c..5d2bad3 100644 --- a/.github/workflows/constraints.txt +++ b/.github/workflows/constraints.txt @@ -1,5 +1,5 @@ -black==24.3.0 -pre-commit==3.7.0 -pip==24.0 -flake8==7.0.0 +black==23.12.0 +pre-commit==3.6.0 +pip==23.3.1 +flake8==6.1.0 reorder-python-imports==3.12.0 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 6d25d81..0410562 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - master jobs: labeler: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 2b6c542..f8d9503 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -3,12 +3,13 @@ on: push: branches: - main + - master jobs: draft_release: name: Release Drafter runs-on: ubuntu-latest steps: - name: Run release-drafter - uses: release-drafter/release-drafter@v6.0.0 + uses: release-drafter/release-drafter@v5.25.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d1ffd81..daf41b0 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - master - dev pull_request: schedule: @@ -21,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v5.1.0 + uses: actions/setup-python@v5.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -35,8 +36,6 @@ jobs: pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports - name: Run pre-commit on all files - env: - SKIP: no-commit-to-branch run: | pre-commit run --all-files --show-diff-on-failure --color=always diff --git a/.gitignore b/.gitignore index fa79e3b..4d18335 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ venv .venv .vscode .DS_Store -.idea* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7903e8..915939b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,9 @@ Contributing to this project should be as easy and transparent as possible, whet - Submitting a fix - Proposing new features -## GitHub is used for everything +## Github is used for everything -GitHub is used to host code, to track issues and feature requests, as well as accept pull requests. +Github is used to host code, to track issues and feature requests, as well as accept pull requests. Pull requests are the best way to propose changes to the codebase. @@ -23,9 +23,9 @@ Pull requests are the best way to propose changes to the codebase. In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. -## Report bugs using GitHub's [issues](../../issues) +## Report bugs using Github's [issues](../../issues) -GitHub's issues are used to track public bugs. +GitHub issues are used to track public bugs. Report a bug by [opening a new issue](../../issues/new/choose); it's that easy! ## Write bug reports with detail, background, and sample code @@ -55,7 +55,7 @@ People _love_ thorough bug reports. I'm not even kidding. Use [black](https://github.com/ambv/black) to make sure the code follows the style. Or use the `pre-commit` settings implemented in this repository -(see dedicated section below). +(see deicated section below). ## Test your code modification diff --git a/README.en.md b/README.en.md index d3d6e2c..e30f360 100644 --- a/README.en.md +++ b/README.en.md @@ -19,15 +19,12 @@ This is a beta release. There will be some bugs, issues, etc. Please bear with u # Hilo [Hilo](https://www.hydroquebec.com/hilo/en/) integration for Home Assistant -# :warning: Breaking change (v2024.2.2 and lower will become unusable) -Hilo's login will stop using "Resource Owner Password Flow" and start using "Authorization Code Flow with PKCE". A few weeks after this change, the current login method will be permanently closed and unusable. - ## Introduction This is the unofficial HACS Hilo integration for Home Assistant. [Hilo](https://www.hiloenergie.com/en-ca/) is a smart home platform developed by an [Hydro Quebec](https://www.hydroquebec.com/hilo/en/) subsidiary. This integration has no direct tie with Hilo or Hydro Quebec. This is a community initiative. Please don't contact -Hilo or Hydro-Quebec with issues with this Home Assistant integration, you can open an issue in the GitHub repository +Hilo or Hydro-Quebec with issues with this Home Assistant integration, you can open an issue in the github repository instead. If you want to help with the development of this integration, you can always submit a feedback form from the Hilo @@ -47,7 +44,7 @@ rewrite it. Hilo is now pushing device readings via websocket from SignalR. ### Features - Support for switches and dimmers as light devices - Get current and set target temperature of thermostat -- Get energy usage of pretty much each device +- Get energy usage of pretty much each devices - Generates energy meters and sensors - Sensor for Hilo Events (challenges) - Sensor for Hilo Gateway @@ -64,11 +61,6 @@ rewrite it. Hilo is now pushing device readings via websocket from SignalR. ## Installation -### Step 0: Compatible install -This custom component requires that Hilo has carried out the install in your home. It will not be possible to set it up otherwise. - -This custom component has been tested to work by various users on HA OS (as bare metal or VM), Docker with the official (ghcr.io) image and Podman. Other types of install may cause permissions issues during creation of a few files by the custom component. - ### Step 1: Download files #### Option 1: Via HACS @@ -85,39 +77,13 @@ Download and copy the `custom_components/hilo` directory from the [latest releas In HA, go to Settings > Devices & Services > Integrations. In the bottom right corner, click the '+ ADD INTEGRATION' button. -![Add Integration](https://github.com/dvd-dev/hilo/assets/108159253/7906f2c9-9547-4478-a625-feaa68e62c5f) - If the component is properly installed, you should be able to find the 'Hilo integration' in the list. You might need to clear you browser cache for the integration to show up. -![Search Integration](https://github.com/dvd-dev/hilo/assets/108159253/1b560a73-042b-46cf-963c-98e5326e98e8) - - -## Configuration (new install) - -The configuration is done in the UI. When you add the integration, you will be redirected to Hilo's website login page to authenticate. - -![Open Website](https://github.com/dvd-dev/hilo/assets/108159253/23b4fb34-f8c3-40b3-8e01-b3e737cc9d44) - - -![Auth Hilo](https://github.com/dvd-dev/hilo/assets/108159253/e4e98b32-78d0-4c49-a2d7-3bd0ae95e9e0) - -You must then accept to link your account. To do so, you must enter your Home Assistant instance's URL or IP address and click Link Account. +## Configuration -![Link](https://github.com/dvd-dev/hilo/assets/108159253/5eb945f7-fa5e-458f-b0fe-ef252aaadf93) - -![Link URL](https://github.com/dvd-dev/hilo/assets/108159253/2c54df64-2e1c-423c-89cf-0eee8f0d4b7b) - -After this, you will be prompted with assigning a room for each one of your devices. - -## Configuration (update from a version earlier than v2024.3.1) - -After update, you will get an error saying you must reauthenticate for the integration to work. - -![Reconfiguration 2](https://github.com/dvd-dev/hilo/assets/108159253/a711d011-17a9-456f-abf6-74cf099014f1) - -![Reath](https://github.com/dvd-dev/hilo/assets/108159253/70118e68-90b9-4667-b056-38ee2cd33133) - -After correctly linking your account like in the previous section, you should see a popup telling you the reauthentification was sucessful. +The configuration is done in the UI. When you add the integration, you will be prompted with your +Hilo username and password. After this, you will be prompted with assigning a room for each one of +your devices. ### Energy meters @@ -199,14 +165,6 @@ Other options are available under the `Configure` button in Home Assistant: - `rate d` - `flex d` -- `appreciation phase`: Integer (hours) - - Add an appreciation phase of X hours before the preheat phase. - -- `pre_cold phase`: Integer (hours) - - Add a cooldown phase of X hours to reduce temperatures before the appreciation phase - - `Scan interval (min: 60s)`: Integer Number of seconds between each device update. Defaults to 60 and it's not recommended to go below 30 as it might @@ -248,61 +206,6 @@ logger: If you have any kind of python/home-assistant experience and want to contribute to the code, feel free to submit a pull request. -### Prepare a dev environment in MacOS / Linux - -1. Prepare necessary directories: -```console -$ HASS_DEV=~/hass-dev/ -$ HASS_RELEASE=2023.12.3 -$ mkdir -p ${HASS_DEV}/config -$ cd $HASS_DEV -$ git clone https://github.com/dvd-dev/hilo.git -$ git clone https://github.com/dvd-dev/python-hilo.git -$ git clone https://github.com/home-assistant/core.git -$ git --git-dir core/ checkout $HASS_RELEASE -``` - -**NOTE**: We also clone home-assistant's core to make it easier to add logging at that level [repo](https://github.com/home-assistant/core). - -2. Launch the container: - -```console -$ docker run -d -p 8123:8123 \ - --name hass \ - -v ${HASS_DEV}/config:/config \ - -v ${HASS_DEV}/python-hilo/pyhilo:/usr/local/lib/python3.11/site-packages/pyhilo:ro \ - -v ${HASS_DEV}/hilo/custom_components/hilo/:/config/custom_components/hilo:ro \ - -v ${HASS_DEV}/core/homeassistant:/usr/src/homeassistant/homeassistant:ro \ - homeassistant/home-assistant:$HASS_RELEASE -``` - -3. Check the container is running - -```console -$ docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -bace2264ee54 homeassistant/home-assistant:2023.12.3 "/init" 3 hours ago Up 28 minutes 0.0.0.0:8123->8123/tcp hass -``` - -4. Check home-assistant logs -```console -$ less ${HASS_DEV}/config/home-assistant.log -$ grep hilo ${HASS_DEV}/config/home-assistant.log -``` - -5. Activate debug logs - -```console -$ cat << EOF >> ${HASS_DEV}/config/configuration.yaml -logger: - default: info - logs: - custom_components.hilo: debug - pyhilo: debug -EOF -$ docker restart hass -``` - ### Before submiting a Pull Request It goes without saying you must test your modifications on your local install for problems. You may modify the .py files inside the following folder. Don't forget a backup! @@ -310,7 +213,7 @@ It goes without saying you must test your modifications on your local install fo custom_components/hilo ``` -If you need to modify python-hilo for your tests, you can pull your own fork into Home Assistant with the following on the CLI: +If you need to modify python-hilo for your tests tests, you can pull your own fork into Home Assistant with the following on the CLI: ``` pip install -e git+https://github.com/YOUR_FORK_HERE/python-hilo.git#egg=python-hilo @@ -344,7 +247,7 @@ git commit -m "I changed this because blabla" ``` git push ``` -- At this point, if you visit the [upstream repository](https://github.com/dvd-dev/hilo), GitHub should prompt you to create a Pull Request (aka PR). Just follow the instructions. +- At this point, if you visit the [upstream repository](https://github.com/dvd-dev/hilo), Github should prompt you to create a Pull Request (aka PR). Just follow the instructions. ### Initial collaborators diff --git a/README.md b/README.md index 369bffc..8043d9e 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,10 @@ Ceci est une version Bêta. Il y aura probablements des bogues, irritants, etc. # Hilo Intégration pour Home Assistant d'[Hilo](https://www.hydroquebec.com/hilo/fr/) -# :warning: Breaking change (v2024.2.2 et antérieures deviendront non-fonctionnelles) -L'authentification (login) de Hilo passera d'une méthode "Resource Owner Password Flow" vers une méthode "Authorization Code Flow with PKCE". Quelques semaines après ce changement, l'ancienne méthode sera fermée définitivement et les anciennes versions de Hilo ne seront plus fonctionnelles. - ## Introduction Ceci est l'intégration HACS non-officielle de Hilo sur Home Assistant. [Hilo](https://www.hiloenergie.com/fr-ca/) est une plateforme de domotique développée par une filliale d'[Hydro-Québec](https://www.hydroquebec.com/hilo/fr/). -Cette intégration n'a aucun liens direct avec Hilo ou Hydro Québec. C'est une initiative communautaire. Merci de ne pas contacter Hilo ou Hydro-Québec pour tout problème avec cette intégration Home Assistant. Vous pouvez ouvrir un "issue" dans ce "repository" github à la place. +Cette intégration n'a aucun liens direct avec Hilo ou Hydro Québec. C'est une initiative communautaire. Merci de ne pas contacter Hilo ou Hydro-Québec pour tout problèmes avec cette intégration Home Assistant. Vous pouvez ouvrir un "issue" dans ce "repository" github à la place. Si vous souhaitez aider avec le développement de cette intégration, vous pouvez toujours soumettre vos commentaires à partir du formulaire de l'app Hilo et demander à ce qu'ils ouvrent leur API publiquement et qu'ils fournissent un environnement de test pour les développeurs. @@ -33,19 +30,19 @@ Si vous souhaitez aider avec le développement de cette intégration, vous pouve Gros merci à [Francis Poisson](https://github.com/francispoisson/) qui est l'auteur de l'intégration originale. Sans le travail qu'il a fait sur cette intégration, je n'aurais probablement jamais considéré utiliser Hilo. -Un autre gros merci à @ic-dev21 pour son implication à plusieurs niveaux. +Un autre gros merci à @ic-dev21 pour son implication à plusieurs niveau. -J'ai décidé de déplacer l'intégration ici, car la dernière mise à jour de Hilo a brisé l'original et j'ai pris le temps de complètement la récrire. Hilo pousse maintenant les lectures des appareils via websocket de SignalR. +J'ai décidé de déplacer l'intégration ici car la dernière mise à jour de Hilo a brisé l'original et j'ai pris le temps de complètement la récrire. Hilo pousse maintenant les lectures des appareils via websocket de SignalR. ### Caractéristiques. - Supporte les interrupteurs et gradateurs en tant que lumières. -- Voir la température actuelle et changer la consigne des thermostats. +- Voir la température actuelle et changer la consigne des thermostat. - Obtenir la consommation énergétique des tous les appareils Hilo. - 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. +- **NOUVEAU**: Mise à jours des lectures plus près du temps réel. ### À faire: - Ajouter la fonctionnalité pour d'autres appareils. @@ -56,11 +53,6 @@ J'ai décidé de déplacer l'intégration ici, car la dernière mise à jour de ## Installation -### Étape 0: Avoir une installation compatible -L'intégration nécessite que l'installation du matériel Hilo soit complétée à votre domicile. Il ne sera pas possible de faire l'installation si ça n'est pas fait. - -Cette intégration a été testée par des utilisateurs sous HA OS (bare metal et VM), Docker avec l'image officielle (ghcr.io), Podman. Tout autre type d'installation peut mener à des problèmes de permission pour certains fichiers créés lors de l'installation initiale du custom_component. - ### Étape 1: Télécharger les fichiers #### Option 1: Via HACS @@ -74,44 +66,19 @@ Télécharger et copier le dossier `custom_components/hilo` de la [dernière ver ### Étape 2: Ajouter l'intégration à HA (<--- étape souvent oubliée) -Dans HA, aller à Paramètres > Appareils et services > Intégrations. +Dans HA, aller à Paramètres > Appareils et services > Intégrations. 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. -![Recherche intégration](https://github.com/dvd-dev/hilo/assets/108159253/7003a402-9369-4063-ac02-709bd0294e42) - -## Configuration (initiale) - -La configuration est faite via l'interface utilisateur. Lorsque vous ajoutez l'intégration, vous êtes redirigés vers le site de connexion d'Hilo afin de vous y authentifier. - -![Auth step 1](https://github.com/dvd-dev/hilo/assets/108159253/d2e396ea-e6df-40e6-9a14-626ef3be87c8) - -![Auth Hilo](https://github.com/dvd-dev/hilo/assets/108159253/e4e98b32-78d0-4c49-a2d7-3bd0ae95e9e0) - -Vous devez ensuite accepter de lier votre compte. Pour ce faire, saisir l'addresse (URL ou IP) de votre instance Home Assistant et appuyez sur Link Account. - -![Link](https://github.com/dvd-dev/hilo/assets/108159253/5eb945f7-fa5e-458f-b0fe-ef252aaadf93) - -Après, vous devrez assigner une pièce de votre maison à chaque appareil. - -## Configuration (mise à jour depuis une version antérieure à v2024.3.1) +## Configuration -Après la mise à jour, vous obtiendrez une erreur comme quoi vous devez vous réauthentifier pour que l'intégration fonctionne. +La configuration est faite via l'interface utilisateur. Lorsque vous ajoutez l'intégration, votre nom d'utilisateur et mot de passe Hio vous seront demandés. Après, vous devrez assigner une pièce de votre maison à chaque appareil. -![Reconfigurer](https://github.com/dvd-dev/hilo/assets/108159253/5b69da7f-d547-4ba7-8b64-8eb1d8f28bdb) - -![Réauthentifier](https://github.com/dvd-dev/hilo/assets/108159253/6b1bf2c3-0d7a-4eb8-815b-594401fc09ef) - -Après avoir lié votre compte comme montré à la section configuration initale, le message suivant apparaîtra. - -![Réauthentifié succès](https://github.com/dvd-dev/hilo/assets/108159253/7708b449-24c3-43c1-843b-8697ae192db1) ### :warning: Compteurs de consommation électrique -La génération automatique des compteurs de consommation électrique est actuellement brisée. J'avais codé ça quand le panneau d'énergie de Homeassistant venait d'être rendu disponible et malheureusement, cette partie du code a changé énormément. Je n'ai plus le temps pour le moment de me remettre la tête là-dedans mais si quelqu'un est assez brave pour se pencher là-dessus en détail, ça va me faire plaisir de merger les patchs. +La génération automatique des compteurs de consommation électrique est actuellement brisée. J'avais codé ça quand le panneau d'énergie de Homeassistant venait d'être rendu disponible et malheureusement, cette parti du code a changé énormément. Je n'ai plus le temps pour le moment de me remettre la tête là dedans mais si quelqu'un est assez brave pour se pencher là dessus en détail, ça va me faire plaisir de merger les patchs. Voir les issues #204 #281 #292 @@ -148,7 +115,7 @@ D'autres options sont disponibles sous le bouton "Configurer" dans Home Assistan - `Intervalle de mise à jour (min: 60s)`: Nombre entier - Nombre de secondes entre chaque mise à jour de l'appareil. Par défaut à 60s. Il n'est pas recommandé d'aller en dessous de 30, car cela pourrait entraîner une suspension de Hilo. Depuis [2023.11.1](https://github.com/dvd-dev/hilo/releases/tag/v2023.11.1) le minimum est passé de 15s à 60s. + Nombre de secondes entre chaque mise à jour de l'appareil. Par défaut à 60s. Il n'est pas recommandé d'aller en dessous de 30 car cela pourrait entraîner une suspension de Hilo. Depuis [2023.11.1](https://github.com/dvd-dev/hilo/releases/tag/v2023.11.1) le minimum est passé de 15s à 60s. ## Exemples d'intégrations Lovelace et d'automatisations @@ -167,7 +134,7 @@ Pour l'instant, voici les liens Swagger que nous avons trouvés: ## FAQ -Vous pouvez trouver la FAQ dans le wiki du projet: https://github.com/dvd-dev/hilo/wiki/FAQ +Vous pouvez trouver le FAQ dans le wiki du projet: https://github.com/dvd-dev/hilo/wiki/FAQ ## Contribuer @@ -176,7 +143,7 @@ Rapporter tout problème est une bonne manière disponible à tous de contribuer Si vous éprouvez des problèmes ou voyez des comportements étranges, merci de soumettre un "Issue" et d'y attach vos journaux. Pour mettre en fonction la journalisation de débogage, vous devez ajouter ceci dans votre fichier `configuration.yaml`: -```yaml +``` logger: default: info logs: @@ -184,81 +151,27 @@ logger: pyhilo: debug ``` -Si vous avez de l'expérience python ou Home Assistant et que vous souhaitez contribuer au code, n'hésitez pas à soumettre une pull request. - -### Préparer un environment de développement sur MacOS / Linux - -1. Preparer les dossiers necessaires: -```console -$ HASS_DEV=~/hass-dev/ -$ HASS_RELEASE=2023.12.3 -$ mkdir -p ${HASS_DEV}/config -$ cd $HASS_DEV -$ git clone https://github.com/dvd-dev/hilo.git -$ git clone https://github.com/dvd-dev/python-hilo.git -$ git clone https://github.com/home-assistant/core.git -$ git --git-dir core/ checkout $HASS_RELEASE -``` - -**NOTE**: On clone aussi le [repo](https://github.com/home-assistant/core) de home-assistant car c'est plus facile d'ajouter du debug à ce niveau. - -2. Lancer le container: - -```console -$ docker run -d -p 8123:8123 \ - --name hass \ - -v ${HASS_DEV}/config:/config \ - -v ${HASS_DEV}/python-hilo/pyhilo:/usr/local/lib/python3.11/site-packages/pyhilo:ro \ - -v ${HASS_DEV}/hilo/custom_components/hilo/:/config/custom_components/hilo:ro \ - -v ${HASS_DEV}/core/homeassistant:/usr/src/homeassistant/homeassistant:ro \ - homeassistant/home-assistant:$HASS_RELEASE -``` - -3. Verifier que le container roule +Si vous avez de l'expérience python ou Home Assistant et que vous souhaitez contribuer au code, n'hésitez pas à soumettre une pull request. -```console -$ docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -bace2264ee54 homeassistant/home-assistant:2023.12.3 "/init" 3 hours ago Up 28 minutes 0.0.0.0:8123->8123/tcp hass -``` +### Avant de soumettre une Pull Request -4. Verifier les logs de home-assistant -```console -$ less ${HASS_DEV}/config/home-assistant.log -$ grep hilo ${HASS_DEV}/config/home-assistant.log +Il va sans dire qu'il est important de tester vos modifications sur une installation locale. Il est possible de modifier les fichiers .py de l'intégration directement dans votre dossier: ``` - -5. Activer les logs debug - -```console -$ cat << EOF >> ${HASS_DEV}/config/configuration.yaml -logger: - default: info - logs: - custom_components.hilo: debug - pyhilo: debug -EOF -$ docker restart hass +custom_components/hilo ``` - -### Avant de soumettre une Pull Request - -Il va sans dire qu'il est important de tester vos modifications sur une installation locale. Il est possible de modifier les fichiers .py de l'intégration directement dans votre dossier `custom_components/hilo`. - N'oubliez pas votre copie de sauvegarde! Si vous devez modifier python-hilo pour vos tests, il est possible d'installer votre "fork" avec la commande suivante dans votre CLI: -```console -$ pip install -e git+https://github.com/VOTRE_FORK_ICI/python-hilo.git#egg=python-hilo +``` +pip install -e git+https://github.com/VOTRE_FORK_ICI/python-hilo.git#egg=python-hilo ``` Vous devrez ensuite redémarrer Home Assistant pour que votre installation prenne effet. Pour revenir en arrière, il suffit de faire: -```console -$ pip install python-hilo ``` - +pip install python-hilo +``` Et redémarrez Home Assistant ### Soumettre une Pull Request @@ -266,24 +179,22 @@ Et redémarrez Home Assistant - D'abord, vous devez créer un `fork` du "repository" dans votre propre espace utilisateur. - Ensuite, vous pouvez en faire un `clone` sur votre ordinateur. - Afin de maintenir une sorte de propreté et de standard dans le code, nous avons des linters et des validateurs qui doivent être exécutés via `pre-commit` hooks: -```console -$ pre-commit install --install-hooks +``` +pre-commit install --install-hooks ``` - Vous pouvez mainteant procéder à votre modification au code. - Lorsque vous avez terminé, vous pouvez `stage` les fichiers pour un `commit`: -```console -$ git add path/to/file +``` +git add path/to/file ``` - Et vous pouvez créer un `commit`: -```console -$ git commit -m "J'ai changé ceci parce que ..." ``` - +git commit -m "J'ai changé ceci parce que ..." +``` - Finalement, vous pouvez `push` le changement vers votre "upstream repository": -```console -$ git push ``` - +git push +``` - Ensuite, si vous visitez le [upstream repository](https://github.com/dvd-dev/hilo), Github devrait vous proposer de créer un "Pull Request" (PR). Vous n'avez qu'à suivre les instructions. ### Collaborateurs initiaux @@ -292,7 +203,7 @@ $ git push * [David Vallee Delisle](https://github.com/valleedelisle/) ### Mentions très honorables -* [Ian Couture](https://github.com/ic-dev21/): Il tient cet addon du bout de ces bras depuis un certain temps +* [Ian Couture](https://github.com/ic-dev21/): Il tiens cet addon du bout de ces bras depuis un certain temps * [Hilo](https://www.hiloenergie.com): Merci à Hilo pour son support et ses contributions. --- diff --git a/custom_components/hilo/__init__.py b/custom_components/hilo/__init__.py index ff9b756..0ad44ac 100755 --- a/custom_components/hilo/__init__.py +++ b/custom_components/hilo/__init__.py @@ -13,22 +13,18 @@ from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_CONNECTIONS, ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_TOKEN, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, Platform, ) from homeassistant.core import Context, Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import ( - aiohttp_client, - config_entry_oauth2_flow, - device_registry as dr, - entity_registry as er, -) +from homeassistant.helpers import aiohttp_client, device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -40,15 +36,14 @@ DataUpdateCoordinator, ) from pyhilo import API +from pyhilo.const import DEFAULT_STATE_FILE from pyhilo.device import HiloDevice from pyhilo.devices import Devices -from pyhilo.event import Event from pyhilo.exceptions import HiloError, InvalidCredentialsError, WebsocketError -from pyhilo.oauth2 import AuthCodeWithPKCEImplementation from pyhilo.util import from_utc_timestamp, time_diff from pyhilo.websocket import WebsocketEvent -from .config_flow import STEP_OPTION_SCHEMA, HiloFlowHandler +from .config_flow import STEP_OPTION_SCHEMA from .const import ( CONF_APPRECIATION_PHASE, CONF_CHALLENGE_LOCK, @@ -130,34 +125,43 @@ async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: """Set up Hilo as config entry.""" - HiloFlowHandler.async_register_implementation( - hass, AuthCodeWithPKCEImplementation(hass) - ) - - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) - - current_options = {**entry.options} - - try: - api = await API.async_create( - session=aiohttp_client.async_get_clientsession(hass), - oauth_session=config_entry_oauth2_flow.OAuth2Session( - hass, entry, implementation - ), - log_traces=current_options.get(CONF_LOG_TRACES, DEFAULT_LOG_TRACES), - ) - except Exception as err: - raise ConfigEntryAuthFailed(err) from err - _async_standardize_config_entry(hass, entry) + current_options = {**entry.options} + log_traces = current_options.get(CONF_LOG_TRACES, DEFAULT_LOG_TRACES) scan_interval = current_options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) scan_interval = ( scan_interval if scan_interval >= MIN_SCAN_INTERVAL else MIN_SCAN_INTERVAL ) + state_yaml = hass.config.path(DEFAULT_STATE_FILE) + + websession = aiohttp_client.async_get_clientsession(hass) + + try: + if entry.data[CONF_TOKEN]: + LOG.debug("Trying auth with token") + api = await API.async_auth_refresh_token( + session=websession, + provided_refresh_token=entry.data[CONF_TOKEN], + log_traces=log_traces, + state_yaml=state_yaml, + ) + else: + raise InvalidCredentialsError + except InvalidCredentialsError as err: + try: + LOG.debug(f"Trying auth with username/password: {err}") + api = await API.async_auth_password( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + session=websession, + log_traces=log_traces, + state_yaml=state_yaml, + ) + except (KeyError, InvalidCredentialsError) as err: + raise ConfigEntryAuthFailed from err + except HiloError as err: + LOG.error("Config entry failed: %s", err) + raise ConfigEntryNotReady from err hilo = Hilo(hass, entry, api) try: @@ -202,25 +206,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_migrate_entry(hass, config_entry: ConfigEntry): - """Migrate old entry.""" - LOG.debug("Migrating from version %s", config_entry.version) - - if config_entry.version > 1: - # This means the user has downgraded from a future version - return False - - if config_entry.version == 1: - config_entry.version = 2 - hass.config_entries.async_update_entry( - config_entry, unique_id="hilo", data={"auth_implementation": "hilo"} - ) - - LOG.debug("Migration to version %s successful", config_entry.version) - - return True - - class Hilo: """Define a Hilo data object.""" @@ -301,7 +286,7 @@ async def on_websocket_event(self, event: WebsocketEvent) -> None: event.arguments[0] ) elif event.target == "DeviceListUpdatedValuesReceived": - # This message only contains display information, such as the Device's name (as set in the app), it's groupid, icon, etc. + # This message only contains display informations, such as the Device's name (as set in the app), it's groupid, icon, etc. # Updating the device name causes issues in the integration, it detects it as a new device and creates a new entity. # Ignore this call, for now... (update_devicelist_from_signalr does work, but causes the issue above) # await self.devices.update_devicelist_from_signalr(event.arguments[0]) @@ -368,39 +353,6 @@ def _get_unknown_source_tracker(self) -> HiloDevice: "sw_version": "0.0.1", } - async def get_event_details(self, event_id: int): - """Getting events from Hilo only when necessary. - Otherwise, we hit the cache. - When preheat is started and our last update is before - the preheat_start, we refresh. This should update the - allowed_kWh, etc. values. - """ - if event_data := self._events.get(event_id): - event = Event(**event_data) - if event.invalid: - LOG.debug( - f"Invalidating cache for event {event_id} during {event.state} phase ({event.current_phase_times=} {event.last_update=})" - ) - del self._events[event_id] - """ - Note ic-dev21: temp fix until we an make it prettier. - During appreciation, pre-heat and reduction we delete - the event attributes and reload them with the next if, - the rest of time time we're reading it from cache - """ - - if event.state in ["appreciation", "pre_heat", "reduction"]: - LOG.debug( - f"Invalidating cache for event {event_id} during appreciation, pre_heat or reduction phase ({event.last_update=})" - ) - del self._events[event_id] - - if event_id not in self._events: - self._events[event_id] = await self._api.get_gd_events( - self.devices.location_id, event_id=event_id - ) - return self._events[event_id] - async def async_init(self, scan_interval: int) -> None: """Initialize the Hilo "manager" class.""" if TYPE_CHECKING: @@ -570,7 +522,8 @@ def check_tarif(self): f"check_tarif: Current plan: {plan_name} Target Tarif: {tarif} Energy used: {energy_used.state} Peak: {self.high_times} {target_cost.state=} {current_state=}" ) known_power = 0 - smart_meter = "sensor.meter00_power" + smart_meter = "sensor.smartenergymeter_power" + smart_meter_alternate = "sensor.meter00_power" unknown_source_tracker = "sensor.unknown_source_tracker_power" for state in self._hass.states.async_all(): entity = state.entity_id @@ -579,27 +532,20 @@ def check_tarif(self): if entity.endswith("_power") and entity not in [ unknown_source_tracker, smart_meter, + smart_meter_alternate, ]: try: known_power += int(float(state.state)) except ValueError: pass - if not entity.endswith("_hilo_energy") or entity.endswith("_cost"): + if not entity.startswith("sensor.hilo_energy") or entity.endswith("_cost"): continue self.fix_utility_sensor(entity, state) if self.track_unknown_sources: total_power = self._hass.states.get(smart_meter) - try: - if known_power <= int(total_power.state): - unknown_power = int(total_power.state) - known_power - else: - unknown_power = 0 - except ValueError: - unknown_power = known_power - LOG.warning( - f"value of total_power ({total_power} not initialized correctly)" - ) - + if not total_power: + total_power = self._hass.states.get(smart_meter_alternate) + unknown_power = int(total_power.state) - known_power self.devices.parse_values_received( [ { @@ -627,21 +573,16 @@ def fix_utility_sensor(self, entity, state): f"fix_utility_sensor(): No source entity defined on {entity}: {current_state}" ) return - - parent_unit_state = self._hass.states.get(attrs.get("source")) - parent_unit = ( - "kWh" - if parent_unit_state is None - else parent_unit_state.attributes.get("unit_of_measurement") - ) + parent_unit = self._hass.states.get(attrs.get("source")) if not parent_unit: LOG.warning( f"fix_utility_sensor(): Unable to find state for parent unit: {current_state}" ) return - new_attrs = { - ATTR_UNIT_OF_MEASUREMENT: parent_unit, # note ic-dev21: now uses parent_unit directly + ATTR_UNIT_OF_MEASUREMENT: parent_unit.as_dict() + .get("attributes", {}) + .get(ATTR_UNIT_OF_MEASUREMENT), ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, } if not all(a in attrs.keys() for a in new_attrs.keys()): @@ -665,64 +606,6 @@ def set_tarif(self, entity, current, new): SELECT_DOMAIN, SERVICE_SELECT_OPTION, data, context=context ) ) - if ( - entity.startswith("select.") - and entity.endswith("_hilo_energy") - and current != new - ): - LOG.debug( - f"check_tarif: Changing tarif of {entity} from {current} to {new}" - ) - context = Context() - data = {ATTR_OPTION: new, "entity_id": entity} - self._hass.async_create_task( - self._hass.services.async_call( - SELECT_DOMAIN, SERVICE_SELECT_OPTION, data, context=context - ) - ) - - @callback - def async_migrate_unique_id( - self, old_unique_id: str, new_unique_id: str | None, platform: str - ) -> None: - """Migrate legacy unique IDs to new format.""" - assert new_unique_id is not None - LOG.debug( - "Checking if unique ID %s on %s needs to be migrated", - old_unique_id, - platform, - ) - entity_registry = er.async_get(self._hass) - # async_get_entity_id wants the "HILO" domain - # in the platform field and the actual platform in the domain - # field for historical reasons since everything used to be - # PLATFORM.INTEGRATION instead of INTEGRATION.PLATFORM - if ( - entity_id := entity_registry.async_get_entity_id( - platform, DOMAIN, old_unique_id - ) - ) is None: - LOG.debug("Unique ID %s does not need to be migrated", old_unique_id) - return - if new_entity_id := entity_registry.async_get_entity_id( - platform, DOMAIN, new_unique_id - ): - LOG.debug( - ( - "Unique ID %s is already in use by %s (system may have been" - " downgraded)" - ), - new_unique_id, - new_entity_id, - ) - return - LOG.debug( - "Migrating unique ID for entity %s (%s -> %s)", - entity_id, - old_unique_id, - new_unique_id, - ) - entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) class HiloEntity(CoordinatorEntity): @@ -749,17 +632,6 @@ def __init__( name=device.name, via_device=(DOMAIN, gateway), ) - try: - mac_address = dr.format_mac(device.sdi) - self._attr_device_info[ATTR_CONNECTIONS] = { - (dr.CONNECTION_NETWORK_MAC, mac_address) - } - except AttributeError: - pass - try: - self._attr_device_info["sw_version"] = device.sw_version - except AttributeError: - pass if not name: name = device.name self._attr_name = name @@ -785,6 +657,13 @@ def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: """Update the entity when new data comes from the websocket.""" raise NotImplementedError() + async def get_event_details(self, event_id: int): + if event_id not in self._events: + self._events[event_id] = await self._api.get_gd_events( + self.devices.location_id, event_id=event_id + ) + return self._events[event_id] + async def async_added_to_hass(self): """Call when entity is added to hass.""" await super().async_added_to_hass() diff --git a/custom_components/hilo/climate.py b/custom_components/hilo/climate.py old mode 100755 new mode 100644 index 7118b5e..4322e3a --- a/custom_components/hilo/climate.py +++ b/custom_components/hilo/climate.py @@ -7,12 +7,7 @@ HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - PRECISION_TENTHS, - Platform, - UnitOfTemperature, -) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -55,17 +50,13 @@ async def async_setup_entry( class HiloClimate(HiloEntity, ClimateEntity): _attr_hvac_modes = [HVACMode.HEAT] - _attr_temperature_unit: str = UnitOfTemperature.CELSIUS + _attr_temperature_unit: str = TEMP_CELSIUS _attr_precision: float = PRECISION_TENTHS _attr_supported_features: int = ClimateEntityFeature.TARGET_TEMPERATURE def __init__(self, hilo: Hilo, device): super().__init__(hilo, device=device, name=device.name) - old_unique_id = f"{slugify(device.name)}-climate" - self._attr_unique_id = f"{slugify(device.identifier)}-climate" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.CLIMATE - ) + self._attr_unique_id = f"{slugify(device.name)}-climate" self.operations = [HVACMode.HEAT] self._has_operation = False self._temperature_entity = None diff --git a/custom_components/hilo/config_flow.py b/custom_components/hilo/config_flow.py index ce4c484..881415b 100755 --- a/custom_components/hilo/config_flow.py +++ b/custom_components/hilo/config_flow.py @@ -1,18 +1,22 @@ """Config flow to configure the Hilo component.""" from __future__ import annotations -import logging from typing import Any from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.const import ( + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import config_validation as cv, selector -from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler -import jwt -from pyhilo.oauth2 import AuthCodeWithPKCEImplementation +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import ConfigType +from pyhilo import API +from pyhilo.exceptions import HiloError, InvalidCredentialsError import voluptuous as vol from .const import ( @@ -22,7 +26,6 @@ CONF_HQ_PLAN_NAME, CONF_LOG_TRACES, CONF_PRE_COLD_PHASE, - CONF_TARIFF, CONF_TRACK_UNKNOWN_SOURCES, CONF_UNTARIFICATED_DEVICES, DEFAULT_APPRECIATION_PHASE, @@ -31,6 +34,7 @@ DEFAULT_HQ_PLAN_NAME, DEFAULT_LOG_TRACES, DEFAULT_PRE_COLD_PHASE, + DEFAULT_SCAN_INTERVAL, DEFAULT_TRACK_UNKNOWN_SOURCES, DEFAULT_UNTARIFICATED_DEVICES, DOMAIN, @@ -38,6 +42,12 @@ MIN_SCAN_INTERVAL, ) +STEP_USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) STEP_OPTION_SCHEMA = vol.Schema( { vol.Optional( @@ -59,11 +69,6 @@ CONF_TRACK_UNKNOWN_SOURCES, default=DEFAULT_TRACK_UNKNOWN_SOURCES, ): cv.boolean, - vol.Optional( - CONF_HQ_PLAN_NAME, default=DEFAULT_HQ_PLAN_NAME - ): selector.SelectSelector( - selector.SelectSelectorConfig(options=list(CONF_TARIFF.keys()), mode="list") - ), vol.Optional( CONF_APPRECIATION_PHASE, default=DEFAULT_APPRECIATION_PHASE, @@ -72,39 +77,26 @@ CONF_PRE_COLD_PHASE, default=DEFAULT_PRE_COLD_PHASE, ): cv.positive_int, - vol.Optional(CONF_SCAN_INTERVAL): ( + vol.Optional(CONF_HQ_PLAN_NAME, default=DEFAULT_HQ_PLAN_NAME): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): ( vol.All(cv.positive_int, vol.Range(min=MIN_SCAN_INTERVAL)) ), } ) -class HiloFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): +class HiloFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Hilo config flow.""" - DOMAIN = DOMAIN - VERSION = 2 - - _reauth_entry: ConfigEntry | None = None - - async def async_step_user(self, user_input=None) -> FlowResult: - """Handle a flow initialized by the user.""" - await self.async_set_unique_id(DOMAIN) - - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - self.async_register_implementation( - self.hass, - AuthCodeWithPKCEImplementation(self.hass), - ) + VERSION = 1 + reauth_entry: ConfigEntry | None = None - return await super().async_step_user(user_input) - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return LOG + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors: dict[str, Any] = {} + self._reauth: bool = False + self._username: str | None = None + self._password: str | None = None @staticmethod @callback @@ -114,38 +106,68 @@ def async_get_options_flow( """Define the config flow to handle options.""" return HiloOptionsFlowHandler(config_entry) - async def async_step_reauth(self, user_input=None) -> FlowResult: - """Perform reauth upon an API authentication error.""" - LOG.debug("async_step_reauth") - self._reauth_entry = self.hass.config_entries.async_get_entry( + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None) -> FlowResult: + async def async_step_reauth_confirm(self, user_input=None): """Dialog that informs the user that reauth is required.""" if user_input is None: - return self.async_show_form( + return self._async_show_form( step_id="reauth_confirm", - data_schema=vol.Schema({}), ) - user_input["implementation"] = DOMAIN - return await super().async_step_user(user_input) + return await self.async_step_user(user_input) - async def async_oauth_create_entry(self, data: dict) -> FlowResult: + async def async_oauth_create_entry(self, data: dict) -> dict: """Create an oauth config entry or update existing entry for reauth.""" - if self._reauth_entry: - self.hass.config_entries.async_update_entry(self._reauth_entry, data=data) - await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + if self.reauth_entry: + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) return self.async_abort(reason="reauth_successful") + await self.async_set_unique_id(data["username"]) + self._abort_if_unique_id_configured() + LOG.debug(f"Creating entry: {data}") + return self.async_create_entry(title=data["username"], data=data) + + def _async_show_form( + self, *, step_id: str = "user", errors: dict[str, Any] | None = None + ) -> FlowResult: + """Show the form.""" + return self.async_show_form( + step_id=step_id, + data_schema=STEP_USER_SCHEMA, + errors=errors or {}, + ) - LOG.debug("Creating entry: %s", data) + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the start of the config flow.""" + if user_input is None: + return self._async_show_form() + errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + hilo = await API.async_auth_password( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + session=session, + ) + except InvalidCredentialsError: + errors = {"base": "invalid_auth"} + except HiloError as err: + LOG.error("Unknown error while logging into Hilo: %s", err) + errors = {"base": "unknown"} - token = data["token"]["access_token"] - decoded_token = jwt.decode(token, options={"verify_signature": False}) - email = decoded_token["email"] + if errors: + return self._async_show_form(errors=errors) - return self.async_create_entry(title=email, data=data) + data = {CONF_USERNAME: hilo._username, CONF_TOKEN: hilo._refresh_token} + return await self.async_oauth_create_entry(data) class HiloOptionsFlowHandler(config_entries.OptionsFlow): @@ -164,7 +186,80 @@ async def async_step_init( return self.async_show_form( step_id="init", - data_schema=self.add_suggested_values_to_schema( - STEP_OPTION_SCHEMA, self.config_entry.options + data_schema=vol.Schema( + { + vol.Optional( + CONF_GENERATE_ENERGY_METERS, + description={ + "suggested_value": self.config_entry.options.get( + CONF_GENERATE_ENERGY_METERS + ) + }, + ): cv.boolean, + vol.Optional( + CONF_UNTARIFICATED_DEVICES, + description={ + "suggested_value": self.config_entry.options.get( + CONF_UNTARIFICATED_DEVICES + ) + }, + ): cv.boolean, + vol.Optional( + CONF_LOG_TRACES, + description={ + "suggested_value": self.config_entry.options.get( + CONF_LOG_TRACES + ) + }, + ): cv.boolean, + vol.Optional( + CONF_CHALLENGE_LOCK, + description={ + "suggested_value": self.config_entry.options.get( + CONF_CHALLENGE_LOCK + ) + }, + ): cv.boolean, + vol.Optional( + CONF_TRACK_UNKNOWN_SOURCES, + description={ + "suggested_value": self.config_entry.options.get( + CONF_TRACK_UNKNOWN_SOURCES + ) + }, + ): cv.boolean, + vol.Optional( + CONF_HQ_PLAN_NAME, + description={ + "suggested_value": self.config_entry.options.get( + CONF_HQ_PLAN_NAME + ) + }, + ): cv.string, + vol.Optional( + CONF_APPRECIATION_PHASE, + description={ + "suggested_value": self.config_entry.options.get( + CONF_APPRECIATION_PHASE + ) + }, + ): cv.positive_int, + vol.Optional( + CONF_PRE_COLD_PHASE, + description={ + "suggested_value": self.config_entry.options.get( + CONF_PRE_COLD_PHASE + ) + }, + ): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, + description={ + "suggested_value": self.config_entry.options.get( + CONF_SCAN_INTERVAL + ) + }, + ): (vol.All(cv.positive_int, vol.Range(min=MIN_SCAN_INTERVAL))), + } ), ) diff --git a/custom_components/hilo/const.py b/custom_components/hilo/const.py index 442aaf5..bea34c4 100755 --- a/custom_components/hilo/const.py +++ b/custom_components/hilo/const.py @@ -37,10 +37,6 @@ DEFAULT_SCAN_INTERVAL = 300 EVENT_SCAN_INTERVAL = 1800 -# During reduction phase, let's refresh the current challenge event -# more often to get the reward numbers -# Note ic-dev21: we'll stay at 300 until proper fix -EVENT_SCAN_INTERVAL_REDUCTION = 300 NOTIFICATION_SCAN_INTERVAL = 1800 MIN_SCAN_INTERVAL = 60 REWARD_SCAN_INTERVAL = 7200 diff --git a/custom_components/hilo/light.py b/custom_components/hilo/light.py index 33e5eec..63ab71e 100644 --- a/custom_components/hilo/light.py +++ b/custom_components/hilo/light.py @@ -5,7 +5,6 @@ LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -31,11 +30,7 @@ async def async_setup_entry( class HiloLight(HiloEntity, LightEntity): def __init__(self, hass: HomeAssistant, hilo: Hilo, device): super().__init__(hilo, device=device, name=device.name) - old_unique_id = f"{slugify(device.name)}-light" - self._attr_unique_id = f"{slugify(device.identifier)}-light" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.LIGHT - ) + self._attr_unique_id = f"{slugify(device.name)}-light" self._debounced_turn_on = Debouncer( hass, LOG, @@ -73,14 +68,13 @@ def color_mode(self): @property def supported_color_modes(self) -> set: """Flag supported modes.""" - color_modes = set() + supports = set() + supports.add(ColorMode.ONOFF) + if self._device.has_attribute("intensity"): + supports.add(ColorMode.BRIGHTNESS) if self._device.has_attribute("hue"): - color_modes.add(ColorMode.HS) - if not color_modes and self._device.has_attribute("intensity"): - color_modes.add(ColorMode.BRIGHTNESS) - if not color_modes: - color_modes.add(ColorMode.ONOFF) - return color_modes + supports.add(ColorMode.HS) + return supports async def async_turn_off(self, **kwargs): LOG.info(f"{self._device._tag} Turning off") diff --git a/custom_components/hilo/managers.py b/custom_components/hilo/managers.py index 788fb8b..b5f0d2a 100644 --- a/custom_components/hilo/managers.py +++ b/custom_components/hilo/managers.py @@ -27,8 +27,7 @@ class UtilityManager: """Class that maps to the utility_meters""" - def __init__(self, hass, period, tariffs): - self.tariffs = tariffs + def __init__(self, hass, period): self.hass = hass self.period = period self.meter_configs = OrderedDict() @@ -59,32 +58,34 @@ def add_meter_config(self, entity, tariff_list, net_consumption): LOG.debug( f"Creating UtilityMeter config: {name} {tariff_list} (Net Consumption: {net_consumption})" ) - self.meter_configs[entity] = OrderedDict( - { - "source": f"sensor.{entity}", - "name": name, - "cycle": self.period, - CONF_TARIFFS: tariff_list, - "net_consumption": net_consumption, - "utility_meter_sensors": [], - "offset": timedelta(0), - "delta_values": False, - "periodically_resetting": True, - "always_available": True, - } - ) + self.meter_configs[entity] = { + CONF_SOURCE_SENSOR: f"sensor.{entity}", + "name": name, + CONF_METER_TYPE: self.period, + CONF_TARIFFS: tariff_list, + CONF_METER_NET_CONSUMPTION: net_consumption, + DATA_TARIFF_SENSORS: [], + CONF_METER_OFFSET: timedelta(0), + CONF_METER_DELTA_VALUES: False, + CONF_METER_PERIODICALLY_RESETTING: True, + } async def update(self, async_add_entities): LOG.debug(f"Setting up UtilityMeter entities {UTILITY_DOMAIN}") if self.new_entities == 0: LOG.debug("No new entities, not setting up again") return - config = { - UTILITY_DOMAIN: OrderedDict( - {**self.hass.data.get("utility_meter_data", {}), **self.meter_configs} - ), - CONF_TARIFFS: self.tariffs, - } + # NOTE(dvd): Pas sur si c'est relevant de pousser les config originale + # current_meters = self.hass.data.get(DATA_UTILITY, {}) + # for meter, conf in current_meters.items(): + # conf[CONF_TARIFFS] = conf.get(CONF_TARIFFS, []) + # conf[CONF_METER_OFFSET] = conf.get(CONF_METER_OFFSET, timedelta(0)) + # conf[CONF_METER_DELTA_VALUES] = conf.get(CONF_METER_DELTA_VALUES, False) + # conf[CONF_METER_TYPE] = conf.get(CONF_METER_TYPE, "daily") + # conf[CONF_METER_NET_CONSUMPTION] = conf.get(CONF_METER_NET_CONSUMPTION, True) + # conf[CONF_METER_PERIODICALLY_RESETTING] = conf.get(CONF_METER_PERIODICALLY_RESETTING, True) + config = {UTILITY_DOMAIN: OrderedDict(self.meter_configs)} + LOG.debug(f"Performing utility_setup: {config=}") await utility_setup(self.hass, config) await utility_setup_platform( self.hass, config, async_add_entities, self.meter_entities diff --git a/custom_components/hilo/manifest.json b/custom_components/hilo/manifest.json index d27a404..c711b7d 100755 --- a/custom_components/hilo/manifest.json +++ b/custom_components/hilo/manifest.json @@ -11,6 +11,6 @@ "documentation": "https://github.com/dvd-dev/hilo", "iot_class": "cloud_push", "issue_tracker": "https://github.com/dvd-dev/hilo/issues", - "requirements": ["python-hilo>=2024.3.1"], - "version": "2024.3.3" + "requirements": ["python-hilo>=2023.12.1"], + "version": "2023.12.1" } diff --git a/custom_components/hilo/sensor.py b/custom_components/hilo/sensor.py index e56b546..d3b3ab9 100755 --- a/custom_components/hilo/sensor.py +++ b/custom_components/hilo/sensor.py @@ -2,7 +2,6 @@ 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 ( @@ -15,13 +14,12 @@ CONCENTRATION_PARTS_PER_MILLION, CONF_SCAN_INTERVAL, CURRENCY_DOLLAR, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, + POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - Platform, - UnitOfEnergy, - UnitOfPower, - UnitOfSoundPressure, - UnitOfTemperature, + SOUND_PRESSURE_DB, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -29,11 +27,9 @@ 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 ( @@ -48,7 +44,7 @@ DEFAULT_SCAN_INTERVAL, DEFAULT_UNTARIFICATED_DEVICES, DOMAIN, - EVENT_SCAN_INTERVAL_REDUCTION, + EVENT_SCAN_INTERVAL, HILO_ENERGY_TOTAL, HILO_SENSOR_CLASSES, LOG, @@ -68,7 +64,7 @@ # From netatmo integration def process_wifi(strength: int) -> str: - """Process Wi-Fi signal strength and return string for display.""" + """Process wifi signal strength and return string for display.""" if strength >= 86: return "Low" if strength >= 71: @@ -108,7 +104,7 @@ def generate_entities_from_device(device, hilo, scan_interval): 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: + if device.has_attribute("power"): entities.append(PowerSensor(hilo, device)) if device.has_attribute("target_temperature"): entities.append(TargetTemperatureSensor(hilo, device)) @@ -117,7 +113,6 @@ def generate_entities_from_device(device, hilo, scan_interval): return entities -# noinspection GrazieInspection async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -143,12 +138,12 @@ async def async_setup_entry( 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) + utility_manager = UtilityManager(hass, energy_meter_period) - def create_energy_entity(hilo, device): - device._energy_entity = EnergySensor(hilo, device) + def create_energy_entity(device): + device._energy_entity = EnergySensor(device) new_entities.append(device._energy_entity) - energy_entity = f"{slugify(device.name)}_hilo_energy" + energy_entity = f"hilo_energy_{slugify(device.name)}" if energy_entity == HILO_ENERGY_TOTAL: LOG.error( "An hilo entity can't be named 'total' because it conflicts " @@ -169,10 +164,10 @@ def create_energy_entity(hilo, device): 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 d.has_attribute("power"): + # If we opt out the geneneration of meters we just create the power sensors if generate_energy_meters: - create_energy_entity(hilo, d) + create_energy_entity(d) async_add_entities(new_entities) if not generate_energy_meters: @@ -210,11 +205,7 @@ class BatterySensor(HiloEntity, SensorEntity): 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 - ) + self._attr_unique_id = f"{slugify(device.name)}-battery" LOG.debug(f"Setting up BatterySensor entity: {self._attr_name}") @property @@ -241,11 +232,7 @@ class Co2Sensor(HiloEntity, SensorEntity): 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 - ) + self._attr_unique_id = f"{slugify(device.name)}-co2" LOG.debug(f"Setting up CO2Sensor entity: {self._attr_name}") @property @@ -291,29 +278,19 @@ class EnergySensor(IntegrationSensor): """Define a Hilo energy sensor entity.""" _attr_device_class = SensorDeviceClass.ENERGY - _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR + _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_state_class = SensorStateClass.TOTAL_INCREASING - _attr_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR _attr_icon = "mdi:lightning-bolt" - def __init__(self, hilo, device): + def __init__(self, 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 - + self._attr_name = f"Hilo Energy {slugify(device.name)}" + self._attr_unique_id = f"hilo_energy_{slugify(device.name)}" + self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + self._unit_prefix = "k" 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 super().__init__( integration_method=METHOD_LEFT, @@ -321,16 +298,18 @@ def __init__(self, hilo, device): round_digits=2, source_entity=self._source, unique_id=self._attr_unique_id, - unit_prefix="k", + unit_prefix=self._unit_prefix, unit_time="h", ) + self._state = 0 + self._last_period = 0 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 + return self._unit_of_measurement async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -367,17 +346,13 @@ def calc_integration(event): class NoiseSensor(HiloEntity, SensorEntity): """Define a Netatmo noise sensor entity.""" - _attr_native_unit_of_measurement = UnitOfSoundPressure.DECIBEL + _attr_native_unit_of_measurement = SOUND_PRESSURE_DB _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 - ) + self._attr_unique_id = f"{slugify(device.name)}-noise" LOG.debug(f"Setting up NoiseSensor entity: {self._attr_name}") @property @@ -397,17 +372,13 @@ class TemperatureSensor(HiloEntity, SensorEntity): """Define a Hilo temperature sensor entity.""" _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_native_unit_of_measurement = TEMP_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 - ) + self._attr_unique_id = f"{slugify(device.name)}-temperature" LOG.debug(f"Setting up TemperatureSensor entity: {self._attr_name}") @property @@ -432,17 +403,13 @@ class TargetTemperatureSensor(HiloEntity, SensorEntity): """Define a Hilo target temperature sensor entity.""" _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS + _attr_native_unit_of_measurement = TEMP_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 - ) + self._attr_unique_id = f"{slugify(device.name)}-target-temperature" LOG.debug(f"Setting up TargetTemperatureSensor entity: {self._attr_name}") @property @@ -464,7 +431,7 @@ def icon(self): class WifiStrengthSensor(HiloEntity, SensorEntity): - """Define a Wi-Fi strength sensor entity.""" + """Define a Wifi strength sensor entity.""" _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -501,13 +468,7 @@ class HiloNotificationSensor(HiloEntity, RestoreEntity, SensorEntity): 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 - ) + self._attr_unique_id = slugify(self._attr_name) LOG.debug(f"Setting up NotificationSensor entity: {self._attr_name}") self.scan_interval = timedelta(seconds=NOTIFICATION_SCAN_INTERVAL) self._state = 0 @@ -572,32 +533,16 @@ class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity): _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 - ) + self._attr_unique_id = slugify(self._attr_name) 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._attr_native_unit_of_measurement = hilo._hass.config.currency self._state = 0 - self._history = self._load_history() + self._history = [] self.async_update = Throttle(self.scan_interval)(self._async_update) @property @@ -692,20 +637,6 @@ async def _async_update(self): 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): @@ -724,16 +655,9 @@ class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity): 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 - ) + self._attr_unique_id = slugify(self._attr_name) 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.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL) self._state = "off" self._next_events = [] self.async_update = Throttle(self.scan_interval)(self._async_update) @@ -798,19 +722,15 @@ async def _async_update(self): 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 + """Devices like the gateway or Smoke Detectors don't have much attributes, + except for the "disonnected" attributes. 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 - ) + self._attr_unique_id = slugify(device.name) LOG.debug(f"Setting up DeviceSensor entity: {self._attr_name}") @property @@ -832,7 +752,7 @@ def icon(self): class HiloCostSensor(HiloEntity, SensorEntity): _attr_device_class = SensorDeviceClass.MONETARY - _attr_native_unit_of_measurement = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}" + _attr_native_unit_of_measurement = f"{CURRENCY_DOLLAR}/{ENERGY_KILO_WATT_HOUR}" _attr_state_class = SensorStateClass.TOTAL _attr_icon = "mdi:cash" @@ -840,18 +760,10 @@ def __init__(self, hilo, name, plan_name, amount=0): device = next((d for d in hilo.devices.all if d.type == "Gateway"), None) 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_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR 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._attr_unique_id = slugify(self._attr_name) self._last_update = dt_util.utcnow() self._cost = amount super().__init__(hilo, name=self._attr_name, device=device) diff --git a/custom_components/hilo/switch.py b/custom_components/hilo/switch.py index 73020f3..7402d15 100644 --- a/custom_components/hilo/switch.py +++ b/custom_components/hilo/switch.py @@ -1,6 +1,5 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify @@ -25,11 +24,7 @@ async def async_setup_entry( class HiloSwitch(HiloEntity, SwitchEntity): def __init__(self, hilo: Hilo, device): super().__init__(hilo, device=device, name=device.name) - old_unique_id = f"{slugify(device.name)}-switch" - self._attr_unique_id = f"{slugify(device.identifier)}-switch" - hilo.async_migrate_unique_id( - old_unique_id, self._attr_unique_id, Platform.SWITCH - ) + self._attr_unique_id = f"{slugify(device.name)}-switch" LOG.debug(f"Setting up Switch entity: {self._attr_name}") @property diff --git a/custom_components/hilo/translations/en.json b/custom_components/hilo/translations/en.json index 59272ca..62104fe 100755 --- a/custom_components/hilo/translations/en.json +++ b/custom_components/hilo/translations/en.json @@ -2,11 +2,11 @@ "config": { "step": { "user": { - "description": "The Hilo integration interacts with the Hilo application. Hilo is a smart home product made by a subsidary of Hydro Quebec." - }, - "reauth_confirm": { - "title": "Reauthenticate integration", - "description": "The integration needs to re-authenticate your account" + "description": "The Hilo integration interacts with the Hilo application. Hilo is a smart home product made by a subsidary of Hydro Quebec.", + "data": { + "username": "Hilo Username", + "password": "Hilo Password" + } } }, "error": { @@ -15,10 +15,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "This Hilo account is already in use.", - "reauth_successful": "Re-authentication successful", - "user_rejected_authorize" : "Account linking rejected", - "single_instance_allowed" : "Already configured. Only a single configuration is possible." + "already_configured": "This Hilo account is already in use." } }, "options": { @@ -39,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/custom_components/hilo/translations/fr.json b/custom_components/hilo/translations/fr.json index 475556b..366ae6b 100755 --- a/custom_components/hilo/translations/fr.json +++ b/custom_components/hilo/translations/fr.json @@ -2,11 +2,11 @@ "config": { "step": { "user": { - "description": "L'intégration Hilo intéragit avec l'application Hilo. Hilo est un produit de domotique fait par une filliale de Hydro Québec." - }, - "reauth_confirm": { - "title": "Réauthentifier l'intégration", - "description": "L'intégration doit réauthentifier votre compte" + "description": "L'intégration Hilo intéragit avec l'application Hilo. Hilo est un produit de domotique fait par une filliale de Hydro Québec.", + "data": { + "username": "Nom d'utilisateur Hilo", + "password": "Mot de passe Hilo" + } } }, "error": { @@ -15,10 +15,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "Ce compte Hilo est déjà utilisé.", - "reauth_successful": "Ré-authentification réussie", - "user_rejected_authorize" : "Association du compte refusée", - "single_instance_allowed" : "Déjà configurée. Une seule configuration possible." + "already_configured": "Ce compte Hilo est déjà utilisé." } }, "options": { @@ -33,10 +30,10 @@ "log_traces": "Enregistrer aussi les requêtes et messages websocket (requiert le niveau de journalisation debug sur L'intégration et pyhilo)", "challenge_lock": "Vérouiller les entités climate lors de défis Hilo, empêchant tout changement lorsqu'un défi est en cours.", "track_unknown_sources": "Suivre des sources de consommation inconnues dans un compteur séparé. Ceci est une approximation calculée à partir de la lecture du compteur intelligent.", - "appreciation_phase": "Ajouter une période d'ancrage de X heures avant la phase de préchauffage.", - "pre_cold_phase": "Ajouter une période de refroidissement de X heures avant la phase d'ancrage." + "appreciation_phase": "Ajouter une période d'appréciation de X heures avant la phase de préchauffage.", + "pre_cold_phase": "Ajouter une période de refroidissement de X heures avant la phase d'appréciation." } } } } -} \ No newline at end of file +} diff --git a/doc/automations/README.md b/doc/automations/README.md index bcd04b2..1ab326f 100644 --- a/doc/automations/README.md +++ b/doc/automations/README.md @@ -1,538 +1 @@ -# Some Hilo automation ideas - -## Notification défis Hilo pendant le défi -Prérequis: sensors template de @Francoloco - -Petite notification à part pour ma femme pour qu'elle sache quand elle peut recommencer à vivre. -``` -alias: Défi Hilo Notification En Cours -description: "" -trigger: - - platform: state - entity_id: - - sensor.defi_hilo - from: scheduled - to: appreciation - id: appreciation - - platform: state - entity_id: - - sensor.defi_hilo - from: appreciation - to: pre_heat - id: pre_heat - - platform: state - entity_id: - - sensor.defi_hilo - from: pre_heat - to: reduction - id: reduction - - platform: state - entity_id: - - sensor.defi_hilo - from: reduction - to: recovery - id: recovery - - platform: state - entity_id: - - sensor.defi_hilo - from: recovery - to: completed - id: completed -condition: - - condition: not - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: scheduled - - condition: template - value_template: > - {{ states('input_text.defi_hilo_last_state_notification') != - states('sensor.defi_hilo') }} -action: - - choose: - - conditions: - - condition: trigger - id: - - appreciation - - pre_heat - - completed - sequence: - - service: notify.mobile_app_REDACTED - data: - title: Défi Hilo - message: Le sensor défi est passé à {{ states('sensor.defi_hilo') }} - - service: input_text.set_value - metadata: {} - data: - value: "{{states('sensor.defi_hilo')}}" - target: - entity_id: input_text.defi_hilo_last_state_notification - - conditions: - - condition: trigger - id: - - reduction - sequence: - - service: notify.mobile_app_REDACTED - data: - title: Défi Hilo - message: >- - Le sensor défi est passé à {{ states('sensor.defi_hilo') }}, le - montant maximal possible est de - {{states('sensor.defi_hilo_allowed_cash')}}$ - - service: input_text.set_value - metadata: {} - data: - value: "{{states('sensor.defi_hilo')}}" - target: - entity_id: input_text.defi_hilo_last_state_notification - - conditions: - - condition: trigger - id: - - recovery - sequence: - - service: notify.mobile_app_iphone_ian - data: - title: Défi Hilo - message: >- - Le sensor défi est passé à {{states('sensor.defi_hilo')}}, le - montant obtenu estimé est de - {{states('sensor.defi_hilo_remaining_cash')}}$ - - service: notify.mobile_app_REDACTED - data: - title: Défi Hilo - message: >- - Le sensor défi est passé à {{ states('sensor.defi_hilo') }} plus - besoin de faire attention - - service: input_text.set_value - metadata: {} - data: - value: "{{states('sensor.defi_hilo')}}" - target: - entity_id: input_text.defi_hilo_last_state_notification -mode: single - -``` - -## Défi Hilo Ancrage AM avec détection de présence - -Des appareils ont été enlevés pour simplifier. - -On trigger à 1h du matin ou quand le sensor tourne à appreciation. Garantie le trigger. - -On vérifie que la liste de défis le prochain est en AM. - -Condition sur le sensor défi car pourrait être en train de switcher d'état à 1h pile. - -Condition d'heure en cas de reboot pas rapport de HA pour par rouler 2 fois. - -Choose selon qu'il y ait quelqu'un à la maison ou pas. - -``` -alias: Défi Hilo Ancrage AM -description: "" -trigger: - - platform: time - at: "1:00:00" - - platform: state - entity_id: - - sensor.defi_hilo - to: appreciation - from: scheduled -condition: - - condition: template - value_template: "{{'am' in state_attr('sensor.defi_hilo','next_events')[0]['period'] }}" - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: appreciation - - condition: state - entity_id: sensor.defi_hilo - state: scheduled - - condition: time - after: "11:55:00" - before: "12:05:00" -action: - - choose: - - conditions: - - condition: numeric_state - entity_id: zone.home - above: 0 - sequence: - - service: climate.set_temperature - data: - temperature: 21 - target: - entity_id: - - climate.thermostat_cuisine - alias: Présent - - conditions: - - condition: numeric_state - entity_id: zone.home - below: 1 - sequence: - - service: climate.set_temperature - data: - temperature: 23 - target: - entity_id: - - climate.thermostat_cuisine - alias: Absent -mode: single -``` -## Défi Hilo Ancrage PM avec détection de présence - -Des appareils ont été enlevés pour simplifier. - -On trigger à 12h ou quand le sensor tourne à appreciation. Garantie le trigger. - -On vérifie que la liste de défis le prochain est en PM. - -Condition sur le sensor défi car pourrait être en train de switcher d'état à 1h pile. - -Condition d'heure en cas de reboot pas rapport de HA pour par rouler 2 fois. - -Choose selon qu'il y ait quelqu'un à la maison ou pas. -``` -alias: Défi Hilo Ancrage PM -description: "" -trigger: - - platform: time - at: "12:00:00" - - platform: state - entity_id: - - sensor.defi_hilo - to: appreciation - from: scheduled -condition: - - condition: template - value_template: "{{'pm' in state_attr('sensor.defi_hilo','next_events')[0]['period'] }}" - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: appreciation - - condition: state - entity_id: sensor.defi_hilo - state: scheduled - - condition: time - after: "11:55:00" - before: "12:05:00" -action: - - choose: - - conditions: - - condition: numeric_state - entity_id: zone.home - above: 0 - sequence: - - service: climate.set_temperature - data: - temperature: 21 - target: - entity_id: - - climate.thermostat_cuisine - alias: Présent - - conditions: - - condition: numeric_state - entity_id: zone.home - below: 1 - sequence: - - service: climate.set_temperature - data: - temperature: 23 - target: - entity_id: - - climate.thermostat_cuisine - alias: Absent -mode: single - -``` - -## Défi Hilo Préchauffe AM -Je coupe mon convectair dans la salle de bain parce qu'il me gosse. - -Je coupe mon échangeur d'air pour garder ma chaleur en dedans. - -Perso je laisse le contrôle à Hilo pour cette phase, mon préchauffage est déjà entamé. -``` -alias: Défi Hilo Préchauffe AM -description: "" -trigger: - - platform: state - entity_id: - - sensor.defi_hilo - to: pre_heat - from: appreciation - enabled: true - - platform: time - at: "04:00:00" -condition: - - condition: time - before: "04:05:00" - after: "03:55:00" - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: pre_heat - - condition: state - entity_id: sensor.defi_hilo - state: reduction -action: - - service: climate.set_temperature - data: - temperature: 15 - target: - entity_id: climate.thermostat_salle_de_bain - - service: switch.turn_off - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air -mode: single - -``` - -## Défi Hilo Réduction AM - -``` -alias: Défi Hilo Réduction AM -description: "" -trigger: - - platform: state - entity_id: - - sensor.defi_hilo - to: reduction - from: pre_heat - enabled: true - - platform: time - at: "06:00:00" -condition: - - condition: time - before: "06:05:00" - after: "05:55:00" - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: pre_heat - - condition: state - entity_id: sensor.defi_hilo - state: reduction -action: - - service: climate.set_temperature - data: - temperature: 15 - target: - entity_id: - - climate.thermostat_cuisine - - service: switch.turn_off - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air -mode: single - -``` - -## Défi Hilo Réduction PM -Petite passe passe ici, j'éteins l'échangeur d'air juste à 7h pour sortir les odeurs du souper pareille. - -``` -alias: Défi Hilo Réduction PM -description: "" -trigger: - - platform: state - entity_id: - - sensor.defi_hilo - to: reduction - from: pre_heat - enabled: true - id: Sensor - - platform: time - at: "17:00:00" - id: 5pm - - platform: time - at: "19:00:00" - id: 7pm -condition: - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: pre_heat - - condition: state - entity_id: sensor.defi_hilo - state: reduction -action: - - choose: - - conditions: - - condition: trigger - id: - - Sensor - - 5pm - - condition: time - before: "17:05:00" - after: "16:55:00" - sequence: - - service: climate.set_temperature - data: - temperature: 15 - target: - entity_id: - - climate.thermostat_cuisine - alias: Réduction - - conditions: - - condition: trigger - id: - - 7pm - sequence: - - service: switch.turn_off - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air -mode: single - -``` - -## Défi Hilo Recovery AM avec détection de présence/dodo travail de nuit -Dépendant s'il y a quelque à la maison ou quelqu'un qui dort à la maison les actions changent. - - -``` -alias: Défi Hilo Recovery AM -description: "" -trigger: - - platform: state - entity_id: - - sensor.defi_hilo - to: recovery - from: reduction - - platform: time - at: "10:00:00" -condition: - - condition: time - before: "10:05:00" - after: "09:55:00" -action: - - choose: - - conditions: - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: reduction - - condition: state - entity_id: sensor.defi_hilo - state: recovery - - condition: numeric_state - entity_id: zone.home - above: 0 - sequence: - - if: - - condition: state - entity_id: input_boolean.REDACTED_dodo_de_jour - state: "off" - then: - - service: climate.set_temperature - data: - temperature: 21 - target: - entity_id: - - climate.thermostat_cuisine - - service: switch.turn_on - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air - else: - - service: climate.set_temperature - data: - temperature: 18 - target: - entity_id: - - climate.thermostat_cuisine - - service: switch.turn_on - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air - - service: climate.set_temperature - data: - temperature: 20.5 - target: - entity_id: climate.thermostat_chambre_des_maitres - - conditions: - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: reduction - - condition: state - entity_id: sensor.defi_hilo - state: recovery - - condition: numeric_state - entity_id: zone.home - below: 1 - sequence: - - service: climate.set_temperature - data: - temperature: 18 - target: - entity_id: - - climate.thermostat_cuisine - - service: switch.turn_on - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air -mode: single - -``` - -## Défi Hilo Recovery PM - -``` -alias: Défi Hilo Recovery PM -description: "" -trigger: - - platform: state - entity_id: - - sensor.defi_hilo - to: recovery - from: reduction - - platform: time - at: "21:00:00" -condition: - - condition: time - before: "21:05:00" - after: "20:55:00" - - condition: or - conditions: - - condition: state - entity_id: sensor.defi_hilo - state: reduction - - condition: state - entity_id: sensor.defi_hilo - state: recovery -action: - - service: climate.set_temperature - data: - temperature: 21 - target: - entity_id: - - climate.thermostat_cuisine - - service: climate.set_temperature - data: - temperature: 20.5 - target: - entity_id: climate.thermostat_chambre_des_maitres - - service: switch.turn_on - metadata: {} - data: {} - target: - entity_id: switch.prise_echangeur_d_air -mode: single - -``` \ No newline at end of file +## Some Hilo automation ideas diff --git a/info.md b/info.md index 1a8ca86..46dd629 100644 --- a/info.md +++ b/info.md @@ -43,13 +43,13 @@ developers. ## Configuration -The configuration is done in the UI. When you add the integration, you will be redirected to Hilo's authentication website. -You have to accept to link your account. After this, you will be prompted with assigning a room for each one of +The configuration is done in the UI. When you add the integration, you will be prompted with your +Hilo username and password. After this, you will be prompted with assigning a room for each one of your devices. ### Energy meters -Energy meters are a new feature of this integration. We used to manually generate them with template sensors and automation, +Energy meters are a new feature of this integration. We used to manually generate them with template sensors and automation but they now have been fully integrated into the Hilo integration. #### Warning @@ -198,7 +198,7 @@ Here's an example on how to add the energy data to Lovelace. ## Credits -Credits to [Francis Poisson](https://github.com/francispoisson/) who's the original author of this integration. +Credits to to [Francis Poisson](https://github.com/francispoisson/) who's the original author of this integration. ---