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

Miio push server #64726

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e4758a6
Add xiaomi miio push server
starkillerOG Jan 22, 2022
7cc2599
fix styling
starkillerOG Jan 22, 2022
0235f38
bug fixes
starkillerOG Jan 22, 2022
4adcdf5
add logger warnings
starkillerOG Jan 22, 2022
3f37e15
fix styling
starkillerOG Jan 22, 2022
79f23a7
add link to instructions
starkillerOG Jan 24, 2022
9d7f1dd
remove the need for the encrypted token
starkillerOG Jan 24, 2022
f3b6807
fix styling
starkillerOG Jan 24, 2022
e0fe336
Merge branch 'dev' into Miio_push
starkillerOG Jul 18, 2022
baca168
Update to merge upstream PR
starkillerOG Jul 18, 2022
b21190d
Generalize push_server use
starkillerOG Jul 18, 2022
dc7aef1
Merge branch 'dev' into Miio_push
starkillerOG Jul 18, 2022
98a18b3
Merge branch 'dev' into Miio_push
starkillerOG Jul 19, 2022
89c83ed
fix styling
starkillerOG Jul 19, 2022
607edb0
fixes after testing
starkillerOG Jul 19, 2022
81783c0
fix tests
starkillerOG Jul 19, 2022
8c5ddb9
use fully async push server
starkillerOG Sep 6, 2022
7008609
device_triggers init
starkillerOG Sep 6, 2022
11ce0c9
refactor events part 2
starkillerOG Sep 6, 2022
aa107a5
Merge branch 'dev' into Miio_push
starkillerOG Sep 6, 2022
3534739
Clean up events
starkillerOG Sep 6, 2022
8ddd89f
add trigger translations
starkillerOG Sep 6, 2022
948f49b
black
starkillerOG Sep 6, 2022
034d17f
disable last event and last press entities by default
starkillerOG Sep 6, 2022
1a14146
fix json
starkillerOG Sep 6, 2022
fdbb0e5
Update .coveragerc
starkillerOG Sep 6, 2022
81f0230
Update homeassistant/components/xiaomi_miio/__init__.py
starkillerOG Sep 12, 2022
5d44e0e
Update homeassistant/components/xiaomi_miio/__init__.py
starkillerOG Sep 12, 2022
de3d57e
Update homeassistant/components/xiaomi_miio/__init__.py
starkillerOG Sep 12, 2022
2526bd8
Update homeassistant/components/xiaomi_miio/alarm_control_panel.py
starkillerOG Sep 12, 2022
340c307
remove last_press and last_event
starkillerOG Sep 12, 2022
fcd5f85
only setup push_server for gateways
starkillerOG Sep 12, 2022
c55b96f
separate device triggers
starkillerOG Sep 12, 2022
adff3eb
fix async_context
starkillerOG Sep 12, 2022
951bdbe
Merge branch 'dev' into Miio_push
starkillerOG Sep 12, 2022
215b056
fix black
starkillerOG Sep 12, 2022
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
96 changes: 92 additions & 4 deletions homeassistant/components/xiaomi_miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@
FanMiot,
FanP5,
FanZA5,
PushServer,
RoborockVacuum,
Timer,
VacuumStatus,
)
from miio.gateway.gateway import GatewayException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TOKEN, Platform
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
CONF_HOST,
CONF_MODEL,
CONF_TOKEN,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
Expand All @@ -47,6 +54,8 @@
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
KEY_PUSH_SERVER,
KEY_PUSH_SERVER_STOP,
MODEL_AIRFRESH_A1,
MODEL_AIRFRESH_T2017,
MODEL_FAN_1C,
Expand Down Expand Up @@ -80,6 +89,7 @@

GATEWAY_PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.LIGHT,
Platform.SENSOR,
Platform.SWITCH,
Expand Down Expand Up @@ -119,6 +129,27 @@
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Xiaomi Miio components from a config entry."""
hass.data.setdefault(DOMAIN, {})
host = entry.data[CONF_HOST]

# Create push server
if (
KEY_PUSH_SERVER not in hass.data[DOMAIN]
and entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY
):
push_server = PushServer(host)
hass.data[DOMAIN][KEY_PUSH_SERVER] = push_server
# start the async push server (only once)
await push_server.start()

# register stop callback to shutdown the push server
async def stop_push_server(event):
"""Stop push server."""
_LOGGER.debug("Shutting down Xiaomi Miio push server")
await push_server.stop()

unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_push_server)
hass.data[DOMAIN][KEY_PUSH_SERVER_STOP] = unsub

if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
await async_setup_gateway_entry(hass, entry)
return True
Expand Down Expand Up @@ -397,8 +428,23 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) ->
raise ConfigEntryNotReady() from error
gateway_info = gateway.gateway_info

def event_callback_factory(device_id):
"""Create event callback for a subdevice."""

@callback
def event_callback(action, params):
"""Event from subdevice."""
_LOGGER.debug("Got new event_callback: %s, %s", action, params)
hass.bus.async_fire(
f"{DOMAIN}_event",
{"device_id": device_id, "type": action, "params": params},
)

return event_callback

device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
# Register gateway and event callbacks
gateway_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, gateway_info.mac_address)},
identifiers={(DOMAIN, gateway_id)},
Expand All @@ -409,6 +455,29 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) ->
hw_version=gateway_info.hardware_version,
)

gateway.gateway_device.register_callback(
f"{gateway_id}_event", event_callback_factory(gateway_entry.id)
)
await gateway.gateway_device.alarm.subscribe_events()

# Register subdevices and event callbacks
for sub_device in gateway.gateway_device.devices.values():
device_entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, sub_device.sid)},
via_device=(DOMAIN, gateway_id),
manufacturer="Xiaomi",
name=sub_device.name,
model=sub_device.model,
sw_version=sub_device.firmware_version,
hw_version=sub_device.zigbee_model,
)

sub_device.register_callback(
f"{sub_device.sid}_event", event_callback_factory(device_entry.id)
)
await sub_device.subscribe_events()

def update_data_factory(sub_device):
"""Create update function for a subdevice."""

Expand Down Expand Up @@ -436,7 +505,7 @@ async def async_update_data():
)

hass.data[DOMAIN][entry.entry_id] = {
CONF_GATEWAY: gateway.gateway_device,
KEY_DEVICE: gateway.gateway_device,
KEY_COORDINATOR: coordinator_dict,
}

Expand Down Expand Up @@ -468,9 +537,28 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry, platforms
)

if KEY_PUSH_SERVER in hass.data[DOMAIN]:
_LOGGER.debug("Removing subscribtions from miio device memory")
push_server = hass.data[DOMAIN][KEY_PUSH_SERVER]
device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
await push_server.unregister_miio_device(device)

if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)

loaded_entries = [
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
if len(loaded_entries) == 1 and KEY_PUSH_SERVER in hass.data[DOMAIN]:
# No miio devices left, stop push server
unsub_stop = hass.data[DOMAIN].pop(KEY_PUSH_SERVER_STOP)
unsub_stop()
_LOGGER.debug("Shutting down Xiaomi Miio push server")
push_server = hass.data[DOMAIN].pop(KEY_PUSH_SERVER)
await push_server.stop()

return unload_ok


Expand Down
24 changes: 21 additions & 3 deletions homeassistant/components/xiaomi_miio/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import CONF_GATEWAY, DOMAIN
from .const import DOMAIN, KEY_DEVICE

_LOGGER = logging.getLogger(__name__)

Expand All @@ -36,7 +37,7 @@ async def async_setup_entry(
) -> None:
"""Set up the Xiaomi Gateway Alarm from a config entry."""
entities = []
gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
entity = XiaomiGatewayAlarm(
gateway,
f"{config_entry.title} Alarm",
Expand Down Expand Up @@ -76,6 +77,23 @@ async def _try_command(self, mask_error, func, *args, **kwargs):
except DeviceException as exc:
_LOGGER.error(mask_error, exc)

@callback
def alarm_callback(self, action, params):
"""Push from gateway."""
if action == "alarm_triggering":
self._attr_state = STATE_ALARM_TRIGGERED
self.async_write_ha_state()

async def async_added_to_hass(self):
"""Subscribe to push server callbacks and install the callbacks on the gateway."""
self._gateway.register_callback(self.unique_id, self.alarm_callback)
await super().async_added_to_hass()

async def async_will_remove_from_hass(self):
"""Unsubscribe callbacks and remove from gateway memory when removed."""
self._gateway.remove_callback(self.unique_id)
await super().async_will_remove_from_hass()

async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Turn on."""
await self._try_command(
Expand Down
43 changes: 42 additions & 1 deletion homeassistant/components/xiaomi_miio/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .const import (
CONF_DEVICE,
CONF_FLOW_TYPE,
CONF_GATEWAY,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
Expand All @@ -34,6 +35,7 @@
MODELS_VACUUM_WITH_SEPARATE_MOP,
)
from .device import XiaomiCoordinatedMiioEntity
from .gateway import XiaomiGatewayDevice

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -81,6 +83,14 @@ class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription):
device_class=BinarySensorDeviceClass.PLUG,
entity_category=EntityCategory.DIAGNOSTIC,
),
XiaomiMiioBinarySensorDescription(
key="is_open",
device_class=BinarySensorDeviceClass.DOOR,
),
XiaomiMiioBinarySensorDescription(
key="motion",
device_class=BinarySensorDeviceClass.MOTION,
),
)

AIRFRESH_A1_BINARY_SENSORS = (ATTR_PTC_STATUS,)
Expand Down Expand Up @@ -175,7 +185,23 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Xiaomi sensor from a config entry."""
entities = []
entities: list[BinarySensorEntity] = []

if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
sub_devices = gateway.devices
for sub_device in sub_devices.values():
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][
sub_device.sid
]
for description in BINARY_SENSOR_TYPES:
if description.key not in sub_device.status:
continue
entities.append(
XiaomiGatewayBinarySensor(
coordinator, sub_device, config_entry, description
)
)

if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE:
model = config_entry.data[CONF_MODEL]
Expand Down Expand Up @@ -245,3 +271,18 @@ def _determine_native_value(self):
return self.entity_description.value(state)

return state


class XiaomiGatewayBinarySensor(XiaomiGatewayDevice, BinarySensorEntity):
"""Representation of a XiaomiGatewayBinarySensor."""

def __init__(self, coordinator, sub_device, entry, description):
"""Initialize the XiaomiSensor."""
super().__init__(coordinator, sub_device, entry)
self._unique_id = f"{sub_device.sid}-{description.key}"
self.entity_description = description

@property
def is_on(self):
"""Return the state of the sensor."""
return self._sub_device.status[self.entity_description.key]
2 changes: 2 additions & 0 deletions homeassistant/components/xiaomi_miio/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
# Keys
KEY_COORDINATOR = "coordinator"
KEY_DEVICE = "device"
KEY_PUSH_SERVER = "push_server"
KEY_PUSH_SERVER_STOP = "push_server_stop"

# Attributes
ATTR_AVAILABLE = "available"
Expand Down
29 changes: 26 additions & 3 deletions homeassistant/components/xiaomi_miio/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from construct.core import ChecksumError
from micloud import MiCloud
from micloud.micloudexception import MiCloudAccessDenied
from miio import DeviceException, gateway
from miio import DeviceException, Gateway
from miio.gateway.gateway import GATEWAY_MODEL_EU

from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity

Expand All @@ -17,6 +18,7 @@
CONF_CLOUD_SUBDEVICES,
CONF_CLOUD_USERNAME,
DOMAIN,
KEY_PUSH_SERVER,
AuthException,
SetupException,
)
Expand All @@ -39,6 +41,7 @@ def __init__(self, hass, config_entry):
self._cloud_country = None
self._host = None
self._token = None
self._push_server = None

@property
def gateway_device(self):
Expand All @@ -60,6 +63,7 @@ async def async_connect_gateway(self, host, token):
self._cloud_username = self._config_entry.data.get(CONF_CLOUD_USERNAME)
self._cloud_password = self._config_entry.data.get(CONF_CLOUD_PASSWORD)
self._cloud_country = self._config_entry.data.get(CONF_CLOUD_COUNTRY)
self._push_server = self._hass.data[DOMAIN][KEY_PUSH_SERVER]

await self._hass.async_add_executor_job(self.connect_gateway)

Expand All @@ -73,7 +77,11 @@ async def async_connect_gateway(self, host, token):
def connect_gateway(self):
"""Connect the gateway in a way that can called by async_add_executor_job."""
try:
self._gateway_device = gateway.Gateway(self._host, self._token)
self._gateway_device = Gateway(
self._host,
self._token,
push_server=self._push_server,
)
# get the gateway info
self._gateway_info = self._gateway_device.info()
except DeviceException as error:
Expand Down Expand Up @@ -138,7 +146,7 @@ def __init__(self, coordinator, sub_device, entry):
self._sub_device = sub_device
self._entry = entry
self._unique_id = sub_device.sid
self._name = f"{sub_device.name} ({sub_device.sid})"
self._name = sub_device.name

@property
def unique_id(self):
Expand Down Expand Up @@ -170,3 +178,18 @@ def available(self):
return False

return self.coordinator.data[ATTR_AVAILABLE]

@callback
def push_callback(self, action, params):
"""Push from subdevice."""
self.async_write_ha_state()

async def async_added_to_hass(self):
"""Subscribe to push server callbacks and install the callbacks on the gateway."""
self._sub_device.register_callback(self.unique_id, self.push_callback)
await super().async_added_to_hass()

async def async_will_remove_from_hass(self):
"""Unsubscribe callbacks and remove from gateway memory when removed."""
self._sub_device.remove_callback(self.unique_id)
await super().async_will_remove_from_hass()
3 changes: 2 additions & 1 deletion homeassistant/components/xiaomi_miio/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
CONF_GATEWAY,
DOMAIN,
KEY_COORDINATOR,
KEY_DEVICE,
MODELS_LIGHT_BULB,
MODELS_LIGHT_CEILING,
MODELS_LIGHT_EYECARE,
Expand Down Expand Up @@ -130,7 +131,7 @@ async def async_setup_entry(
light: MiioDevice

if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
# Gateway light
if gateway.model not in [
GATEWAY_MODEL_AC_V1,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/xiaomi_miio/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ async def async_setup_entry(
entities: list[SensorEntity] = []

if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY:
gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY]
gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE]
# Gateway illuminance sensor
if gateway.model not in [
GATEWAY_MODEL_AC_V1,
Expand Down
Loading