Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: add scene history sensor #1361

Merged
merged 9 commits into from
Nov 21, 2023
Merged
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
2 changes: 2 additions & 0 deletions custom_components/xiaomi_miot/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ async def async_step_cloud(self, user_input=None):
vol.In(CONN_MODES),
vol.Optional('renew_devices', default=user_input.get('renew_devices', False)): bool,
vol.Optional('disable_message', default=user_input.get('disable_message', False)): bool,
vol.Optional('disable_scene_history', default=user_input.get('disable_scene_history', False)): bool,
})
return self.async_show_form(
step_id='cloud',
Expand All @@ -646,6 +647,7 @@ async def async_step_cloud_filter(self, user_input=None):
cfg.update({
CONF_CONN_MODE: prev_input.get(CONF_CONN_MODE),
'disable_message': prev_input.get('disable_message'),
'disable_scene_history': prev_input.get('disable_scene_history'),
**(user_input or {}),
})
self.hass.config_entries.async_update_entry(
Expand Down
167 changes: 160 additions & 7 deletions custom_components/xiaomi_miot/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
from typing import cast
from datetime import datetime, timedelta
from functools import partial
from functools import partial, cmp_to_key

from homeassistant.const import * # noqa: F401
from homeassistant.helpers.entity import (
Expand Down Expand Up @@ -63,12 +63,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
cfg = hass.data[DOMAIN].get(config_entry.entry_id) or {}
mic = cfg.get(CONF_XIAOMI_CLOUD)
config_data = config_entry.data or {}
if isinstance(mic, MiotCloud) and mic.user_id and not config_data.get('disable_message'):
hass.data[DOMAIN]['accounts'].setdefault(mic.user_id, {})
if not hass.data[DOMAIN]['accounts'][mic.user_id].get('messenger'):
entity = MihomeMessageSensor(hass, mic)
hass.data[DOMAIN]['accounts'][mic.user_id]['messenger'] = entity
async_add_entities([entity], update_before_add=False)

if isinstance(mic, MiotCloud) and mic.user_id:
if not config_data.get('disable_message'):
hass.data[DOMAIN]['accounts'].setdefault(mic.user_id, {})

if not hass.data[DOMAIN]['accounts'][mic.user_id].get('messenger'):
entity = MihomeMessageSensor(hass, mic)
hass.data[DOMAIN]['accounts'][mic.user_id]['messenger'] = entity
async_add_entities([entity], update_before_add=False)

if not config_data.get('disable_scene_history'):
al-one marked this conversation as resolved.
Show resolved Hide resolved
homes = await mic.async_get_homerooms()
for home in homes:
home_id = home.get('id')
if hass.data[DOMAIN]['accounts'][mic.user_id].get(f'scene_history_{home_id}'):
continue

entity = MihomeSceneHistorySensor(hass, mic, home_id, home.get('uid'))
hass.data[DOMAIN]['accounts'][mic.user_id][f'scene_history_{home_id}'] = entity
async_add_entities([entity], update_before_add=False)

await async_setup_config_entry(hass, config_entry, async_setup_platform, async_add_entities, ENTITY_DOMAIN)


Expand Down Expand Up @@ -711,6 +726,144 @@ async def fetch_latest_message(self):
return msg


class MihomeSceneHistorySensor(MiCoordinatorEntity, SensorEntity, RestoreEntity):
MESSAGE_TIMEOUT = 60
UPDATE_INTERVAL = 15

_has_none_message = False

def __init__(self, hass, cloud: MiotCloud, home_id, owner_user_id):
self.hass = hass
self.cloud = cloud
self.home_id = int(home_id)
self.owner_user_id = int(owner_user_id)
self.entity_id = f'{ENTITY_DOMAIN}.mi_{cloud.user_id}_{home_id}_scene_history'
self._attr_unique_id = f'{DOMAIN}-mihome-scene-history-{cloud.user_id}_{home_id}'
self._attr_name = f'Xiaomi {cloud.user_id}_{home_id} Scene History'
self._attr_icon = 'mdi:message'
self._attr_should_poll = False
self._attr_native_value = None
self._attr_extra_state_attributes = {
'entity_class': self.__class__.__name__,
}
self.coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=self._attr_unique_id,
update_method=self.fetch_latest_message,
update_interval=timedelta(seconds=self.UPDATE_INTERVAL),
)
super().__init__(self.coordinator)

async def async_added_to_hass(self):
await super().async_added_to_hass()
self.hass.data[DOMAIN]['entities'][self.entity_id] = self
if sec := self.custom_config_integer('interval_seconds'):
self.coordinator.update_interval = timedelta(seconds=sec)

if restored := await self.async_get_last_extra_data():
restored_dict = restored.as_dict()

attrs = restored_dict.get('attrs', {})
if ts := attrs.get('ts'):
attrs['timestamp'] = datetime.fromtimestamp(ts, local_zone()) if ts else None

_LOGGER.debug(
'xiaomi scene history %s %d, async_added_to_hass restore state: state= %s attrs= %s',
self.cloud.user_id, self.home_id, restored_dict.get('state'), attrs,
)
self._attr_native_value = restored_dict.get('state')
self._attr_extra_state_attributes.update(attrs)

await self.coordinator.async_config_entry_first_refresh()

async def async_will_remove_from_hass(self):
"""Run when entity will be removed from hass.
To be extended by integrations.
"""
await super().async_will_remove_from_hass()
self.hass.data[DOMAIN]['accounts'].get(self.cloud.user_id, {}).pop(f'scene_history_{self.home_id}', None)

@property
def extra_restore_state_data(self):
"""Return entity specific state data to be restored."""
return RestoredExtraData({
'state': self.native_value,
'attrs': self._attr_extra_state_attributes,
})

def trim_message(self, msg):
ts = msg.get('time') or int(time.time())
return {
"from": msg.get('from'),
"name": msg.get('name'),
"ts": ts,
'timestamp': datetime.fromtimestamp(ts, local_zone()),
"scene_id": str(msg.get('userSceneId')),
"targets": msg.get('msg', []),
}

@staticmethod
def _cmp_message(a, b):
a_ts, b_ts = a.get('ts', 0), b.get('ts', 0)
if a_ts != b_ts:
return a_ts - b_ts

a_scene_id, b_scene_id = a.get('scene_id', 0), b.get('scene_id', 0)
if a_scene_id < b_scene_id:
return -1
if a_scene_id > b_scene_id:
return 1
return 0

async def async_set_message(self, msg):
self._attr_native_value = msg.get('name')
_LOGGER.debug('New xiaomi scene history for %s %d: %s', self.cloud.user_id, self.home_id, self._attr_native_value)

old = self._attr_extra_state_attributes or {}
self._attr_extra_state_attributes.update({**msg, 'prev_value': old.get('name'), 'prev_scene_id': old.get('scene_id')})

async def fetch_latest_message(self):
res = await self.cloud.async_request_api('scene/history', data={
"home_id": self.home_id,
"uid": int(self.cloud.user_id),
"owner_uid": self.owner_user_id,
"command": "history",
"limit": 15,
}) or {}

messages = [self.trim_message(msg) for msg in (res.get('result') or {}).get('history') or []]
if not messages:
if not self._has_none_message:
_LOGGER.warning('Get xiaomi scene history for %s %d failed: %s', self.cloud.user_id, self.home_id, res)

self._has_none_message = True
return {}

messages.sort(key=cmp_to_key(self._cmp_message), reverse=False)
_LOGGER.debug(
'Get xiaomi scene history for %s %d success: prev_timestamp= %d prev_scene_id= %s messages= %s,',
self.cloud.user_id, self.home_id,
self._attr_extra_state_attributes.get('ts') or 0,
self._attr_extra_state_attributes.get('scene_id') or '',
messages,
)

must_after = int(time.time()) - self.MESSAGE_TIMEOUT
for msg in messages:
if msg.get('ts') < must_after:
continue

if self._cmp_message(msg, self._attr_extra_state_attributes) <= 0:
continue

await self.async_set_message(msg)
self._has_none_message = False
return msg

return {}


class XiaoaiConversationSensor(MiCoordinatorEntity, BaseSensorSubEntity):
def __init__(self, parent, hass, option=None):
BaseSensorSubEntity.__init__(self, parent, 'conversation', option)
Expand Down
3 changes: 2 additions & 1 deletion custom_components/xiaomi_miot/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"server_country": "Server location of MiCloud",
"conn_mode": "Connection mode for device",
"renew_devices": "Force renew devices",
"disable_message": "Disable Mihome notification sensor"
"disable_message": "Disable Mihome notification sensor",
"disable_scene_history": "Disable Mihome scene history sensor"
}
},
"cloud_filter": {
Expand Down
3 changes: 2 additions & 1 deletion custom_components/xiaomi_miot/translations/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
"server_country": "小米服务器",
"conn_mode": "设备连接模式",
"renew_devices": "更新设备列表",
"disable_message": "禁用米家APP通知消息实体"
"disable_message": "禁用米家APP通知消息实体",
"disable_scene_history": "禁用米家场景历史实体"
}
},
"cloud_filter": {
Expand Down