parent
769513d6a8
commit
82f9de31b1
|
@ -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():
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
):
|
||||
|
|
Loading…
Reference in New Issue