Motion Blinds upgrade to local push (#44391)

* Motion Blinds upgrade to local push
pull/44498/head
starkillerOG 2020-12-24 00:15:11 +01:00 committed by GitHub
parent 769513d6a8
commit 82f9de31b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 136 additions and 24 deletions

View File

@ -1,22 +1,29 @@
"""The motion_blinds component."""
from asyncio import TimeoutError as AsyncioTimeoutError
import asyncio
from datetime import timedelta
import logging
from socket import timeout
from motionblinds import MotionMulticast
from homeassistant import config_entries, core
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER
from .const import (
DOMAIN,
KEY_COORDINATOR,
KEY_GATEWAY,
KEY_MULTICAST_LISTENER,
MANUFACTURER,
MOTION_PLATFORMS,
)
from .gateway import ConnectMotionGateway
_LOGGER = logging.getLogger(__name__)
MOTION_PLATFORMS = ["cover", "sensor"]
async def async_setup(hass: core.HomeAssistant, config: dict):
"""Set up the Motion Blinds component."""
@ -31,8 +38,23 @@ async def async_setup_entry(
host = entry.data[CONF_HOST]
key = entry.data[CONF_API_KEY]
# Create multicast Listener
if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]:
multicast = MotionMulticast()
hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast
# start listening for local pushes (only once)
await hass.async_add_executor_job(multicast.Start_listen)
# register stop callback to shutdown listening for local pushes
def stop_motion_multicast(event):
"""Stop multicast thread."""
_LOGGER.debug("Shutting down Motion Listener")
multicast.Stop_listen()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_motion_multicast)
# Connect to motion gateway
connect_gateway_class = ConnectMotionGateway(hass)
connect_gateway_class = ConnectMotionGateway(hass, multicast)
if not await connect_gateway_class.async_connect_gateway(host, key):
raise ConfigEntryNotReady
motion_gateway = connect_gateway_class.gateway_device
@ -41,14 +63,19 @@ async def async_setup_entry(
"""Call all updates using one async_add_executor_job."""
motion_gateway.Update()
for blind in motion_gateway.device_list.values():
blind.Update()
try:
blind.Update()
except timeout:
# let the error be logged and handled by the motionblinds library
pass
async def async_update_data():
"""Fetch data from the gateway and blinds."""
try:
await hass.async_add_executor_job(update_gateway)
except timeout as socket_timeout:
raise AsyncioTimeoutError from socket_timeout
except timeout:
# let the error be logged and handled by the motionblinds library
pass
coordinator = DataUpdateCoordinator(
hass,
@ -57,7 +84,7 @@ async def async_setup_entry(
name=entry.title,
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=10),
update_interval=timedelta(seconds=600),
)
# Fetch initial data so we have data when entities subscribe
@ -91,11 +118,22 @@ async def async_unload_entry(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_forward_entry_unload(
config_entry, "cover"
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in MOTION_PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
if len(hass.data[DOMAIN]) == 1:
# No motion gateways left, stop Motion multicast
_LOGGER.debug("Shutting down Motion Listener")
multicast = hass.data[DOMAIN].pop(KEY_MULTICAST_LISTENER)
await hass.async_add_executor_job(multicast.Stop_listen)
return unload_ok

View File

@ -7,12 +7,11 @@ from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_HOST
# pylint: disable=unused-import
from .const import DOMAIN
from .const import DEFAULT_GATEWAY_NAME, DOMAIN
from .gateway import ConnectMotionGateway
_LOGGER = logging.getLogger(__name__)
DEFAULT_GATEWAY_NAME = "Motion Gateway"
CONFIG_SCHEMA = vol.Schema(
{
@ -26,7 +25,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Motion Blinds config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the Motion Blinds flow."""
@ -48,7 +47,7 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_connect(self, user_input=None):
"""Connect to the Motion Gateway."""
connect_gateway_class = ConnectMotionGateway(self.hass)
connect_gateway_class = ConnectMotionGateway(self.hass, None)
if not await connect_gateway_class.async_connect_gateway(self.host, self.key):
return self.async_abort(reason="connection_error")
motion_gateway = connect_gateway_class.gateway_device

View File

@ -1,6 +1,10 @@
"""Constants for the Motion Blinds component."""
DOMAIN = "motion_blinds"
MANUFACTURER = "Motion, Coulisse B.V."
MANUFACTURER = "Motion Blinds, Coulisse B.V."
DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway"
MOTION_PLATFORMS = ["cover", "sensor"]
KEY_GATEWAY = "gateway"
KEY_COORDINATOR = "coordinator"
KEY_MULTICAST_LISTENER = "multicast_listener"

View File

@ -15,6 +15,7 @@ from homeassistant.components.cover import (
DEVICE_CLASS_SHUTTER,
CoverEntity,
)
from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER
@ -125,6 +126,11 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity):
"""Return the name of the blind."""
return f"{self._blind.blind_type}-{self._blind.mac[12:]}"
@property
def available(self):
"""Return True if entity is available."""
return self._blind.available
@property
def current_cover_position(self):
"""
@ -146,6 +152,21 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity):
"""Return if the cover is closed or not."""
return self._blind.position == 100
@callback
def _push_callback(self):
"""Update entity state when a push has been received."""
self.schedule_update_ha_state(force_refresh=False)
async def async_added_to_hass(self):
"""Subscribe to multicast pushes."""
self._blind.Register_callback(self.unique_id, self._push_callback)
await super().async_added_to_hass()
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
self._blind.Remove_callback(self.unique_id)
await super().async_will_remove_from_hass()
def open_cover(self, **kwargs):
"""Open the cover."""
self._blind.Open()

View File

@ -10,9 +10,10 @@ _LOGGER = logging.getLogger(__name__)
class ConnectMotionGateway:
"""Class to async connect to a Motion Gateway."""
def __init__(self, hass):
def __init__(self, hass, multicast):
"""Initialize the entity."""
self._hass = hass
self._multicast = multicast
self._gateway_device = None
@property
@ -24,11 +25,15 @@ class ConnectMotionGateway:
"""Update all information of the gateway."""
self.gateway_device.GetDeviceList()
self.gateway_device.Update()
for blind in self.gateway_device.device_list.values():
blind.Update_from_cache()
async def async_connect_gateway(self, host, key):
"""Connect to the Motion Gateway."""
_LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3])
self._gateway_device = MotionGateway(ip=host, key=key)
self._gateway_device = MotionGateway(
ip=host, key=key, multicast=self._multicast
)
try:
# update device info and get the connected sub devices
await self._hass.async_add_executor_job(self.update_gateway)

View File

@ -3,6 +3,6 @@
"name": "Motion Blinds",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
"requirements": ["motionblinds==0.1.6"],
"requirements": ["motionblinds==0.4.7"],
"codeowners": ["@starkillerOG"]
}
}

View File

@ -9,6 +9,7 @@ from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
)
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -71,6 +72,11 @@ class MotionBatterySensor(CoordinatorEntity, Entity):
"""Return the name of the blind battery sensor."""
return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}"
@property
def available(self):
"""Return True if entity is available."""
return self._blind.available
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
@ -91,6 +97,21 @@ class MotionBatterySensor(CoordinatorEntity, Entity):
"""Return device specific state attributes."""
return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage}
@callback
def push_callback(self):
"""Update entity state when a push has been received."""
self.schedule_update_ha_state(force_refresh=False)
async def async_added_to_hass(self):
"""Subscribe to multicast pushes."""
self._blind.Register_callback(self.unique_id, self.push_callback)
await super().async_added_to_hass()
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
self._blind.Remove_callback(self.unique_id)
await super().async_will_remove_from_hass()
class MotionTDBUBatterySensor(MotionBatterySensor):
"""
@ -160,6 +181,11 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity):
return "Motion gateway signal strength"
return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}"
@property
def available(self):
"""Return True if entity is available."""
return self._device.available
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
@ -179,3 +205,18 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity):
def state(self):
"""Return the state of the sensor."""
return self._device.RSSI
@callback
def push_callback(self):
"""Update entity state when a push has been received."""
self.schedule_update_ha_state(force_refresh=False)
async def async_added_to_hass(self):
"""Subscribe to multicast pushes."""
self._device.Register_callback(self.unique_id, self.push_callback)
await super().async_added_to_hass()
async def async_will_remove_from_hass(self):
"""Unsubscribe when removed."""
self._device.Remove_callback(self.unique_id)
await super().async_will_remove_from_hass()

View File

@ -952,7 +952,7 @@ minio==4.0.9
mitemp_bt==0.0.3
# homeassistant.components.motion_blinds
motionblinds==0.1.6
motionblinds==0.4.7
# homeassistant.components.tts
mutagen==1.45.1

View File

@ -474,7 +474,7 @@ millheater==0.4.0
minio==4.0.9
# homeassistant.components.motion_blinds
motionblinds==0.1.6
motionblinds==0.4.7
# homeassistant.components.tts
mutagen==1.45.1

View File

@ -8,10 +8,11 @@ from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_N
from homeassistant.components.motion_blinds.const import DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_HOST
from tests.async_mock import patch
from tests.async_mock import Mock, patch
TEST_HOST = "1.2.3.4"
TEST_API_KEY = "12ab345c-d67e-8f"
TEST_DEVICE_LIST = {"mac": Mock()}
@pytest.fixture(name="motion_blinds_connect", autouse=True)
@ -23,6 +24,9 @@ def motion_blinds_connect_fixture():
), patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.Update",
return_value=True,
), patch(
"homeassistant.components.motion_blinds.gateway.MotionGateway.device_list",
TEST_DEVICE_LIST,
), patch(
"homeassistant.components.motion_blinds.async_setup_entry", return_value=True
):