Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions custom_components/xiaomi_miot/core/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
get_value,
DeviceException,
is_offline_exception,
normalize_power_cost_value,
power_cost_period,
update_attrs_with_suffix,
)
from .templates import template
Expand Down Expand Up @@ -1214,13 +1216,45 @@ async def update_cloud_statistics(self, commands=None):
attrs[anm] = rls
elif isinstance(rls, dict):
update_attrs_with_suffix(attrs, rls)
attrs = self._filter_power_cost_statistics(attrs, dt.now())
if attrs:
self.available = True
self.props.update(attrs)
self.data['updated'] = dt.now()
self.dispatch(self.decode_attrs(attrs))
return attrs

def _filter_power_cost_statistics(self, attrs, now):
"""Filter invalid and decreasing power cost statistics."""
result = dict(attrs)
periods = self.data.setdefault('_power_cost_periods', {})
for key in list(result):
period = power_cost_period(key, now)
if not period:
continue
value = normalize_power_cost_value(result[key])
if value is None:
result.pop(key)
continue
previous = normalize_power_cost_value(self.props.get(key))
if (
periods.get(key) == period
and previous is not None
and value < previous
):
self.log.warning(
'Ignore decreasing power cost in the same period: '
'%s: %s -> %s, period=%s',
key,
previous,
value,
period,
)
result.pop(key)
continue
periods[key] = period
return result

@cached_property
def miio_cloud_records(self):
return self.custom_config_list('miio_cloud_records') or []
Expand Down
18 changes: 10 additions & 8 deletions custom_components/xiaomi_miot/core/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
"'target_temperature': state[6:8] | int(0,16),"
"'load_power': results[2] | default(0) | float | round(2),"
"} }}",
'micloud_statistics_power_cost': "{%- set dat = namespace(today=0,month=0) %}"
'micloud_statistics_power_cost': "{%- set dat = namespace(today=none,month=none) %}"
"{%- set tim = now() %}"
"{%- set stm = tim - timedelta(minutes=tim.minute,seconds=tim.second) %}"
"{%- set tod = stm - timedelta(hours=tim.hour) %}"
Expand All @@ -114,17 +114,19 @@
"{%- for d in (result or []) %}"
"{%- set t = d.time | default(0) | int(0) %}"
"{%- if t >= stm %}"
"{%- set v = (d.value | default('[]') | string | from_json) or [] %}"
"{%- set n = v[0] | default(0) %}"
"{%- if t >= tod %}"
"{%- set dat.today = n %}"
"{%- set v = (d.value | default('[]', true) | string | from_json) or [] %}"
"{%- set n = v[0] | default(none) %}"
"{%- if n is number and n is not boolean and n >= 0 %}"
"{%- if t >= tod %}"
"{%- set dat.today = n %}"
"{%- endif %}"
"{%- set dat.month = (dat.month or 0) + n %}"
"{%- endif %}"
"{%- set dat.month = dat.month + n %}"
"{%- endif %}"
"{%- endfor %}"
"{{ {"
"'power_cost_today': dat.today | round(3),"
"'power_cost_month': dat.month | round(3),"
"'power_cost_today': dat.today | round(3) if dat.today is number else none,"
"'power_cost_month': dat.month | round(3) if dat.month is number else none,"
"} }}",
'midr_rv_mirror_cloud_props': "{%- set sta = props.get('prop.Status',0) | int %}"
"{%- set pos = props.get('prop.Position','{}') | from_json %}"
Expand Down
26 changes: 26 additions & 0 deletions custom_components/xiaomi_miot/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import os
import re
import json
import math
import locale
import aiohttp
import asyncio
import tzlocal
import logging
import fnmatch
import voluptuous as vol
from datetime import datetime
from typing import Type, Tuple, Optional, Callable, Set
from functools import wraps
from homeassistant.core import HomeAssistant, split_entity_id # noqa
Expand All @@ -20,6 +22,30 @@
from .const import DOMAIN, DEVICE_CUSTOMIZES, DATA_CUSTOMIZE
from .translation_languages import TRANSLATION_LANGUAGES

POWER_COST_PATTERN = re.compile(
r'(?:^|\.)(power_cost_(today|month)(?:_\d+)?)$'
)


def power_cost_period(attribute: str, timestamp: datetime) -> str | None:
"""Return the local reset period for a power cost attribute."""
if match := POWER_COST_PATTERN.search(attribute):
return timestamp.strftime(
'%Y-%m-%d' if match.group(2) == 'today' else '%Y-%m'
)
return None


def normalize_power_cost_value(value) -> float | None:
"""Return a finite non-negative power cost value."""
if isinstance(value, bool):
return None
try:
value = float(value)
except (TypeError, ValueError):
return None
return value if math.isfinite(value) and value >= 0 else None


def get_value(obj, key, def_value=None, sep='.'):
keys = f'{key}'.split(sep)
Expand Down
76 changes: 75 additions & 1 deletion custom_components/xiaomi_miot/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_time_change
from homeassistant.helpers.restore_state import RestoreEntity, RestoredExtraData
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

Expand All @@ -33,7 +35,13 @@
MiotSpec,
MiotService,
)
from .core.utils import local_zone, get_translation
from .core.utils import (
POWER_COST_PATTERN,
get_translation,
local_zone,
normalize_power_cost_value,
power_cost_period,
)

_LOGGER = logging.getLogger(__name__)
DATA_KEY = f'{ENTITY_DOMAIN}.{DOMAIN}'
Expand Down Expand Up @@ -130,6 +138,47 @@ def on_init(self):
def get_state(self) -> dict:
return {self.attr: self._attr_native_value}

async def async_added_to_hass(self):
await super().async_added_to_hass()
if not POWER_COST_PATTERN.search(self.attr):
return
now = datetime.now(local_zone(self.hass))
current_period = power_cost_period(self.attr, now)
self._power_cost_period = current_period
restored = await self.async_get_last_state()
if restored:
restored_value = normalize_power_cost_value(restored.state)
restored_at = restored.last_changed.astimezone(
local_zone(self.hass)
)
restored_period = power_cost_period(
self.attr,
restored_at,
)
if restored_value is None or restored_period != current_period:
self._attr_native_value = None
else:
self._attr_native_value = restored_value
self.async_on_remove(
async_track_time_change(
self.hass,
self._reset_power_cost_period,
hour=0,
minute=0,
second=0,
)
)

@callback
def _reset_power_cost_period(self, now: datetime):
"""Reset accumulated power at an observed local period boundary."""
period = power_cost_period(self.attr, now)
if not period or period == self._power_cost_period:
return
self._power_cost_period = period
self._attr_native_value = 0
self.async_write_ha_state()

def set_state(self, data: dict):
value = self.conv.value_from_dict(data)
prop = self._miot_property
Expand All @@ -144,8 +193,33 @@ def set_state(self, data: dict):
value = round(float(value), 3)
except (TypeError, ValueError):
value = None
period = power_cost_period(
self.attr,
datetime.now(local_zone(self.hass)),
)
if period:
value = normalize_power_cost_value(value)
if value is None:
return
if self.device_class == SensorDeviceClass.TIMESTAMP:
value = datetime_with_tzinfo(value)
previous = getattr(self, '_attr_native_value', None)
if (
period
and getattr(self, '_power_cost_period', None) == period
and previous is not None
and value < previous
):
self.log.warning(
'Ignore decreasing power cost in the same period: '
'%s: %s -> %s',
self.attr,
previous,
value,
)
return
if period:
self._power_cost_period = period
self._attr_native_value = value

@cached_property
Expand Down