187 files

This commit is contained in:
Home Assistant Version Control
2026-10-09 11:57:31 +00:00
parent 28fa34d78a
commit 1e1b12d52b
187 changed files with 40899 additions and 32654 deletions
+71 -54
View File
@@ -22,6 +22,8 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
__short_version__ as current_version,
)
@@ -35,6 +37,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util
from packaging.version import Version
from pyhilo import API
from pyhilo.device import HiloDevice
@@ -417,6 +420,7 @@ class Hilo:
self.generate_energy_meters = entry.options.get(
CONF_GENERATE_ENERGY_METERS, DEFAULT_GENERATE_ENERGY_METERS
)
self.cost_sensors: dict = {}
# This will get filled in by async_init:
self.coordinator: DataUpdateCoordinator | None = None
self.unknown_tracker_device: HiloDevice | None = None
@@ -1047,42 +1051,56 @@ class Hilo:
)
return challenge_sensor.state == "reduction"
def check_season(self):
def check_season(self, current_time: datetime | None = None) -> bool:
"""Determine if we are using a winter or summer rate."""
current_month = datetime.now().month
if current_time is None:
current_time = dt_util.now()
current_month = current_time.month
LOG.debug("check_season current month is %s", current_month)
return current_month in [12, 1, 2, 3]
@property
def plan_name(self) -> str:
"""Determine which plan is active depending on season and user-selected rate."""
if self.hq_plan_name == "flex d":
if self.check_season():
return "flex d"
return "rate d"
return self.hq_plan_name
def get_tariff_config(self) -> dict[str, float]:
"""Return the active tariff configuration based on plan and season.
For Flex D in non-winter (summer) months, Hydro-Québec bills at Rate D
rates for baseline consumption, while peak events do not occur. We keep
the high rate and reward rate from Flex D so that the utility-meter
tariff lists and dashboard entities remain consistent across seasons,
only swapping in Rate D's low and medium rates.
"""
if self.hq_plan_name == "flex d":
config = CONF_TARIFF["flex d"].copy()
if not self.check_season():
rate_d = CONF_TARIFF["rate d"]
config["low"] = rate_d["low"]
config["medium"] = rate_d["medium"]
return config
return CONF_TARIFF.get(self.hq_plan_name, CONF_TARIFF["rate d"]).copy()
def check_tarif(self):
"""Determine which tarif to select depending on season and user-selected rate."""
if self.generate_energy_meters:
season = self.check_season()
LOG.debug("check_tarif current season state is %s", season)
tarif = "low"
base_sensor = f"sensor.{HILO_ENERGY_TOTAL}_low"
energy_used = self._hass.states.get(base_sensor)
if not energy_used:
LOG.warning("check_tarif: Unable to find state for %s", base_sensor)
return tarif
user_selected_plan_name = self.hq_plan_name
if not self.generate_energy_meters:
return
if user_selected_plan_name == "flex d":
if season:
plan_name = "flex d"
else:
plan_name = "rate d"
else:
plan_name = user_selected_plan_name
tarif_config = CONF_TARIFF.get(plan_name)
tarif_config = self.get_tariff_config()
for tarif_name, rate in tarif_config.items():
if rate > 0 and tarif_name in ["low", "medium", "high"]:
if hasattr(self, "cost_sensors") and tarif_name in self.cost_sensors:
if tarif_name in self.cost_sensors:
sensor = self.cost_sensors[tarif_name]
if sensor._cost != rate:
sensor._cost = rate
sensor.async_write_ha_state()
if sensor.hass is not None:
sensor.async_write_ha_state()
LOG.debug(
"check_tarif Updated %s sensor from %s to %s",
tarif_name,
@@ -1090,47 +1108,46 @@ class Hilo:
rate,
)
current_cost = self._hass.states.get("sensor.hilo_rate_current")
if not current_cost:
LOG.warning(
"check_tarif: Unable to find state for sensor.hilo_rate_current"
)
return
try:
if float(energy_used.state) >= tarif_config.get("low_threshold"):
tarif = "medium"
except ValueError:
LOG.warning(
"Unable to restore a valid state of %s: %s",
tarif = "low"
base_sensor = f"sensor.{HILO_ENERGY_TOTAL}_low"
energy_used = self._hass.states.get(base_sensor)
if energy_used and energy_used.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
try:
if float(energy_used.state) >= tarif_config["low_threshold"]:
tarif = "medium"
except (ValueError, TypeError):
LOG.warning(
"Unable to restore a valid state of %s: %s",
base_sensor,
energy_used.state,
)
else:
LOG.debug(
"check_tarif: %s state not available yet, defaulting to low",
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 not target_cost:
LOG.warning("check_tarif: sensor.hilo_rate_%s not available yet", tarif)
return
if target_cost.state != current_cost.state:
LOG.debug(
"check_tarif: Updating current cost, was %s now %s",
current_cost.state,
target_cost.state,
)
self.set_state("sensor.hilo_rate_current", target_cost.state)
if "current" in self.cost_sensors:
self.cost_sensors["current"]._cost = target_cost.state
target_cost_rate = tarif_config[tarif]
if "current" in self.cost_sensors:
sensor = self.cost_sensors["current"]
if sensor._cost != target_cost_rate:
LOG.debug(
"check_tarif: Updating current cost, was %s now %s",
sensor._cost,
target_cost_rate,
)
sensor._cost = target_cost_rate
if sensor.hass is not None:
sensor.async_write_ha_state()
LOG.debug(
"check_tarif: Current plan: %s Target Tarif: %s Energy used: %s Peak: %s",
plan_name,
self.plan_name,
tarif,
energy_used.state,
energy_used.state if energy_used else "N/A",
self.high_times,
)
+2 -2
View File
@@ -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>=2026.9.1"],
"version": "2026.9.5"
"requirements": ["python-hilo==2026.9.1"],
"version": "2026.10.1"
}
+12 -34
View File
@@ -43,7 +43,6 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
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
@@ -60,7 +59,6 @@ from .const import (
CONF_ENERGY_METER_PERIOD,
CONF_GENERATE_ENERGY_METERS,
CONF_HQ_PLAN_NAME,
CONF_TARIFF,
CONF_UNTARIFICATED_DEVICES,
DEFAULT_ENERGY_METER_PERIOD,
DEFAULT_GENERATE_ENERGY_METERS,
@@ -162,7 +160,7 @@ async def async_setup_entry(
generate_energy_meters = entry.options.get(
CONF_GENERATE_ENERGY_METERS, DEFAULT_GENERATE_ENERGY_METERS
)
tariff_config = CONF_TARIFF.get(hq_plan_name)
tariff_config = hilo.get_tariff_config()
if untarificated_devices:
default_tariff_list = ["total"]
else:
@@ -210,7 +208,10 @@ async def async_setup_entry(
cost_entities.append(sensor)
hilo.cost_sensors[tarif] = sensor # Stores reference for check_tarif
hilo_rate_current = HiloCostSensor(hilo, "Hilo rate current", hq_plan_name)
initial_current_cost = tariff_config.get("low", 0)
hilo_rate_current = HiloCostSensor(
hilo, "Hilo rate current", hq_plan_name, initial_current_cost
)
cost_entities.append(hilo_rate_current)
# Create hilo_rate_current_total sensor that includes access rate per hour
@@ -227,9 +228,6 @@ async def async_setup_entry(
hilo.cost_sensors["current"] = hilo_rate_current
async_add_entities(cost_entities)
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)
@@ -1294,30 +1292,6 @@ class HiloCostSensor(HiloEntity, SensorEntity):
"Initializing energy cost sensor %s %s Amount: %s", name, plan_name, amount
)
def _handle_state_change(self, event):
LOG.debug("_handle_state_change() %s | %s ", self, self._last_update)
if (state := event.data.get("new_state")) is None:
return
now = dt_util.utcnow()
try:
if (
state.attributes.get("hilo_update")
and self._last_update + timedelta(seconds=30) < now
):
LOG.debug(
"Setting new state %s state=%s state.attributes=%s",
state.state,
state,
state.attributes,
)
self._cost = state.state
self._last_update = now
except ValueError:
LOG.error(
"Invalidate state received for %s: %s", self._attr_unique_id, state
)
@property
def state(self):
"""Return the cost."""
@@ -1372,7 +1346,6 @@ class HiloCostTotalSensor(HiloEntity, SensorEntity):
self._attr_native_unit_of_measurement = "CAD"
self._attr_name = name
self.plan_name = plan_name
self._tariff_config = tariff_config
self._access_rate = tariff_config.get("access", 0)
self._last_update = dt_util.utcnow()
old_unique_id = slugify(self._attr_name)
@@ -1394,12 +1367,17 @@ class HiloCostTotalSensor(HiloEntity, SensorEntity):
"""Gateway value updates don't need to trigger a cost refresh."""
return
@property
def tariff_config(self):
"""Return the active tariff configuration."""
return self._hilo.get_tariff_config()
@property
def state(self):
"""Return the total cost in dollars."""
total = 0.0
for tarif in ["low", "medium", "high"]:
rate = self._tariff_config.get(tarif, 0)
rate = self.tariff_config.get(tarif, 0)
if rate <= 0:
continue
energy_entity = f"sensor.{HILO_ENERGY_TOTAL}_{tarif}"
@@ -1440,7 +1418,7 @@ class HiloCostTotalSensor(HiloEntity, SensorEntity):
"last_update": self._last_update,
}
for tarif in ["low", "medium", "high"]:
rate = self._tariff_config.get(tarif, 0)
rate = self.tariff_config.get(tarif, 0)
if rate > 0:
energy_entity = f"sensor.{HILO_ENERGY_TOTAL}_{tarif}"
energy_state = self._hilo._hass.states.get(energy_entity)