Skip to content

Commit

Permalink
Changing the way we keep track of state for CostSensor
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
valleedelisle committed Dec 7, 2023
1 parent f24d83a commit 1225417
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 83 deletions.
39 changes: 24 additions & 15 deletions custom_components/hilo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,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:
Expand All @@ -465,14 +465,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):
Expand All @@ -484,32 +485,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"
Expand Down Expand Up @@ -554,15 +558,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()
Expand All @@ -572,7 +581,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)

Expand All @@ -582,7 +591,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}
Expand Down
156 changes: 88 additions & 68 deletions custom_components/hilo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -566,57 +573,57 @@ 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,
)

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()

events.append(event)
season["events"] = events
new_history.append(season)
self._history = new_history
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()

events.append(event)
season["events"] = events
new_history.append(season)
self._history = new_history


class HiloChallengeSensor(HiloEntity, RestoreEntity, SensorEntity):
Expand Down Expand Up @@ -727,43 +734,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()

0 comments on commit 1225417

Please sign in to comment.