mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add Motion Blinds integration (#42989)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
@ -542,6 +542,10 @@ omit =
|
||||
homeassistant/components/modbus/cover.py
|
||||
homeassistant/components/modbus/switch.py
|
||||
homeassistant/components/modem_callerid/sensor.py
|
||||
homeassistant/components/motion_blinds/__init__.py
|
||||
homeassistant/components/motion_blinds/const.py
|
||||
homeassistant/components/motion_blinds/cover.py
|
||||
homeassistant/components/motion_blinds/sensor.py
|
||||
homeassistant/components/mpchc/media_player.py
|
||||
homeassistant/components/mpd/media_player.py
|
||||
homeassistant/components/mqtt_room/sensor.py
|
||||
|
@ -277,6 +277,7 @@ homeassistant/components/mobile_app/* @robbiet480
|
||||
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
|
||||
homeassistant/components/monoprice/* @etsinko @OnFreund
|
||||
homeassistant/components/moon/* @fabaff
|
||||
homeassistant/components/motion_blinds/* @starkillerOG
|
||||
homeassistant/components/mpd/* @fabaff
|
||||
homeassistant/components/mqtt/* @home-assistant/core @emontnemery
|
||||
homeassistant/components/msteams/* @peroyvind
|
||||
|
101
homeassistant/components/motion_blinds/__init__.py
Normal file
101
homeassistant/components/motion_blinds/__init__.py
Normal file
@ -0,0 +1,101 @@
|
||||
"""The motion_blinds component."""
|
||||
from asyncio import TimeoutError as AsyncioTimeoutError
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from socket import timeout
|
||||
|
||||
from homeassistant import config_entries, core
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
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 .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."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Set up the motion_blinds components from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
host = entry.data[CONF_HOST]
|
||||
key = entry.data[CONF_API_KEY]
|
||||
|
||||
# Connect to motion gateway
|
||||
connect_gateway_class = ConnectMotionGateway(hass)
|
||||
if not await connect_gateway_class.async_connect_gateway(host, key):
|
||||
raise ConfigEntryNotReady
|
||||
motion_gateway = connect_gateway_class.gateway_device
|
||||
|
||||
def update_gateway():
|
||||
"""Call all updates using one async_add_executor_job."""
|
||||
motion_gateway.Update()
|
||||
for blind in motion_gateway.device_list.values():
|
||||
blind.Update()
|
||||
|
||||
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
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name=entry.title,
|
||||
update_method=async_update_data,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=timedelta(seconds=10),
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_refresh()
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
KEY_GATEWAY: motion_gateway,
|
||||
KEY_COORDINATOR: coordinator,
|
||||
}
|
||||
|
||||
device_registry = await dr.async_get_registry(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)},
|
||||
identifiers={(DOMAIN, entry.unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=entry.title,
|
||||
model="Wi-Fi bridge",
|
||||
sw_version=motion_gateway.protocol,
|
||||
)
|
||||
|
||||
for component in MOTION_PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
64
homeassistant/components/motion_blinds/config_flow.py
Normal file
64
homeassistant/components/motion_blinds/config_flow.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""Config flow to configure Motion Blinds using their WLAN API."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from .const import DOMAIN
|
||||
from .gateway import ConnectMotionGateway
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_GATEWAY_NAME = "Motion Gateway"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Motion Blinds config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Motion Blinds flow."""
|
||||
self.host = None
|
||||
self.key = None
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
self.host = user_input[CONF_HOST]
|
||||
self.key = user_input[CONF_API_KEY]
|
||||
return await self.async_step_connect()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_connect(self, user_input=None):
|
||||
"""Connect to the Motion Gateway."""
|
||||
|
||||
connect_gateway_class = ConnectMotionGateway(self.hass)
|
||||
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
|
||||
|
||||
mac_address = motion_gateway.mac
|
||||
|
||||
await self.async_set_unique_id(mac_address)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_GATEWAY_NAME,
|
||||
data={CONF_HOST: self.host, CONF_API_KEY: self.key},
|
||||
)
|
6
homeassistant/components/motion_blinds/const.py
Normal file
6
homeassistant/components/motion_blinds/const.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Constants for the Motion Blinds component."""
|
||||
DOMAIN = "motion_blinds"
|
||||
MANUFACTURER = "Motion, Coulisse B.V."
|
||||
|
||||
KEY_GATEWAY = "gateway"
|
||||
KEY_COORDINATOR = "coordinator"
|
256
homeassistant/components/motion_blinds/cover.py
Normal file
256
homeassistant/components/motion_blinds/cover.py
Normal file
@ -0,0 +1,256 @@
|
||||
"""Support for Motion Blinds using their WLAN API."""
|
||||
|
||||
import logging
|
||||
|
||||
from motionblinds import BlindType
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
DEVICE_CLASS_AWNING,
|
||||
DEVICE_CLASS_BLIND,
|
||||
DEVICE_CLASS_CURTAIN,
|
||||
DEVICE_CLASS_GATE,
|
||||
DEVICE_CLASS_SHADE,
|
||||
DEVICE_CLASS_SHUTTER,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
POSITION_DEVICE_MAP = {
|
||||
BlindType.RollerBlind: DEVICE_CLASS_SHADE,
|
||||
BlindType.RomanBlind: DEVICE_CLASS_SHADE,
|
||||
BlindType.HoneycombBlind: DEVICE_CLASS_SHADE,
|
||||
BlindType.DimmingBlind: DEVICE_CLASS_SHADE,
|
||||
BlindType.DayNightBlind: DEVICE_CLASS_SHADE,
|
||||
BlindType.RollerShutter: DEVICE_CLASS_SHUTTER,
|
||||
BlindType.Switch: DEVICE_CLASS_SHUTTER,
|
||||
BlindType.RollerGate: DEVICE_CLASS_GATE,
|
||||
BlindType.Awning: DEVICE_CLASS_AWNING,
|
||||
BlindType.Curtain: DEVICE_CLASS_CURTAIN,
|
||||
BlindType.CurtainLeft: DEVICE_CLASS_CURTAIN,
|
||||
BlindType.CurtainRight: DEVICE_CLASS_CURTAIN,
|
||||
}
|
||||
|
||||
TILT_DEVICE_MAP = {
|
||||
BlindType.VenetianBlind: DEVICE_CLASS_BLIND,
|
||||
BlindType.ShangriLaBlind: DEVICE_CLASS_BLIND,
|
||||
BlindType.DoubleRoller: DEVICE_CLASS_SHADE,
|
||||
}
|
||||
|
||||
TDBU_DEVICE_MAP = {
|
||||
BlindType.TopDownBottomUp: DEVICE_CLASS_SHADE,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Motion Blind from a config entry."""
|
||||
entities = []
|
||||
motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
|
||||
for blind in motion_gateway.device_list.values():
|
||||
if blind.type in POSITION_DEVICE_MAP:
|
||||
entities.append(
|
||||
MotionPositionDevice(
|
||||
coordinator, blind, POSITION_DEVICE_MAP[blind.type], config_entry
|
||||
)
|
||||
)
|
||||
|
||||
elif blind.type in TILT_DEVICE_MAP:
|
||||
entities.append(
|
||||
MotionTiltDevice(
|
||||
coordinator, blind, TILT_DEVICE_MAP[blind.type], config_entry
|
||||
)
|
||||
)
|
||||
|
||||
elif blind.type in TDBU_DEVICE_MAP:
|
||||
entities.append(
|
||||
MotionTDBUDevice(
|
||||
coordinator, blind, TDBU_DEVICE_MAP[blind.type], config_entry, "Top"
|
||||
)
|
||||
)
|
||||
entities.append(
|
||||
MotionTDBUDevice(
|
||||
coordinator,
|
||||
blind,
|
||||
TDBU_DEVICE_MAP[blind.type],
|
||||
config_entry,
|
||||
"Bottom",
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
_LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MotionPositionDevice(CoordinatorEntity, CoverEntity):
|
||||
"""Representation of a Motion Blind Device."""
|
||||
|
||||
def __init__(self, coordinator, blind, device_class, config_entry):
|
||||
"""Initialize the blind."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._blind = blind
|
||||
self._device_class = device_class
|
||||
self._config_entry = config_entry
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the blind."""
|
||||
return self._blind.mac
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info of the blind."""
|
||||
device_info = {
|
||||
"identifiers": {(DOMAIN, self._blind.mac)},
|
||||
"manufacturer": MANUFACTURER,
|
||||
"name": f"{self._blind.blind_type}-{self._blind.mac[12:]}",
|
||||
"model": self._blind.blind_type,
|
||||
"via_device": (DOMAIN, self._config_entry.unique_id),
|
||||
}
|
||||
|
||||
return device_info
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blind."""
|
||||
return f"{self._blind.blind_type}-{self._blind.mac[12:]}"
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""
|
||||
Return current position of cover.
|
||||
|
||||
None is unknown, 0 is open, 100 is closed.
|
||||
"""
|
||||
if self._blind.position is None:
|
||||
return None
|
||||
return 100 - self._blind.position
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
return self._blind.position == 100
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._blind.Open()
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close cover."""
|
||||
self._blind.Close()
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._blind.Set_position(100 - position)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._blind.Stop()
|
||||
|
||||
|
||||
class MotionTiltDevice(MotionPositionDevice):
|
||||
"""Representation of a Motion Blind Device."""
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""
|
||||
Return current angle of cover.
|
||||
|
||||
None is unknown, 0 is closed/minimum tilt, 100 is fully open/maximum tilt.
|
||||
"""
|
||||
if self._blind.angle is None:
|
||||
return None
|
||||
return self._blind.angle * 100 / 180
|
||||
|
||||
def open_cover_tilt(self, **kwargs):
|
||||
"""Open the cover tilt."""
|
||||
self._blind.Set_angle(180)
|
||||
|
||||
def close_cover_tilt(self, **kwargs):
|
||||
"""Close the cover tilt."""
|
||||
self._blind.Set_angle(0)
|
||||
|
||||
def set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
|
||||
self._blind.Set_angle(angle)
|
||||
|
||||
def stop_cover_tilt(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._blind.Stop()
|
||||
|
||||
|
||||
class MotionTDBUDevice(MotionPositionDevice):
|
||||
"""Representation of a Motion Top Down Bottom Up blind Device."""
|
||||
|
||||
def __init__(self, coordinator, blind, device_class, config_entry, motor):
|
||||
"""Initialize the blind."""
|
||||
super().__init__(coordinator, blind, device_class, config_entry)
|
||||
self._motor = motor
|
||||
self._motor_key = motor[0]
|
||||
|
||||
if self._motor not in ["Bottom", "Top"]:
|
||||
_LOGGER.error("Unknown motor '%s'", self._motor)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the blind."""
|
||||
return f"{self._blind.mac}-{self._motor}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blind."""
|
||||
return f"{self._blind.blind_type}-{self._motor}-{self._blind.mac[12:]}"
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""
|
||||
Return current position of cover.
|
||||
|
||||
None is unknown, 0 is open, 100 is closed.
|
||||
"""
|
||||
if self._blind.position is None:
|
||||
return None
|
||||
|
||||
return 100 - self._blind.position[self._motor_key]
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed or not."""
|
||||
if self._blind.position is None:
|
||||
return None
|
||||
|
||||
return self._blind.position[self._motor_key] == 100
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._blind.Open(motor=self._motor_key)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close cover."""
|
||||
self._blind.Close(motor=self._motor_key)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._blind.Set_position(100 - position, motor=self._motor_key)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._blind.Stop(motor=self._motor_key)
|
45
homeassistant/components/motion_blinds/gateway.py
Normal file
45
homeassistant/components/motion_blinds/gateway.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Code to handle a Motion Gateway."""
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from motionblinds import MotionGateway
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConnectMotionGateway:
|
||||
"""Class to async connect to a Motion Gateway."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the entity."""
|
||||
self._hass = hass
|
||||
self._gateway_device = None
|
||||
|
||||
@property
|
||||
def gateway_device(self):
|
||||
"""Return the class containing all connections to the gateway."""
|
||||
return self._gateway_device
|
||||
|
||||
def update_gateway(self):
|
||||
"""Update all information of the gateway."""
|
||||
self.gateway_device.GetDeviceList()
|
||||
self.gateway_device.Update()
|
||||
|
||||
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)
|
||||
try:
|
||||
# update device info and get the connected sub devices
|
||||
await self._hass.async_add_executor_job(self.update_gateway)
|
||||
except socket.timeout:
|
||||
_LOGGER.error(
|
||||
"Timeout trying to connect to Motion Gateway with host %s", host
|
||||
)
|
||||
return False
|
||||
_LOGGER.debug(
|
||||
"Motion gateway mac: %s, protocol: %s detected",
|
||||
self.gateway_device.mac,
|
||||
self.gateway_device.protocol,
|
||||
)
|
||||
return True
|
8
homeassistant/components/motion_blinds/manifest.json
Normal file
8
homeassistant/components/motion_blinds/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "motion_blinds",
|
||||
"name": "Motion Blinds",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"requirements": ["motionblinds==0.1.6"],
|
||||
"codeowners": ["@starkillerOG"]
|
||||
}
|
181
homeassistant/components/motion_blinds/sensor.py
Normal file
181
homeassistant/components/motion_blinds/sensor.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""Support for Motion Blinds sensors."""
|
||||
import logging
|
||||
|
||||
from motionblinds import BlindType
|
||||
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BATTERY_VOLTAGE = "battery_voltage"
|
||||
TYPE_BLIND = "blind"
|
||||
TYPE_GATEWAY = "gateway"
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Perform the setup for Motion Blinds."""
|
||||
entities = []
|
||||
motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
|
||||
for blind in motion_gateway.device_list.values():
|
||||
entities.append(MotionSignalStrengthSensor(coordinator, blind, TYPE_BLIND))
|
||||
if blind.type == BlindType.TopDownBottomUp:
|
||||
entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom"))
|
||||
entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top"))
|
||||
elif blind.battery_voltage > 0:
|
||||
# Only add battery powered blinds
|
||||
entities.append(MotionBatterySensor(coordinator, blind))
|
||||
|
||||
entities.append(
|
||||
MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MotionBatterySensor(CoordinatorEntity, Entity):
|
||||
"""
|
||||
Representation of a Motion Battery Sensor.
|
||||
|
||||
Updates are done by the cover platform.
|
||||
"""
|
||||
|
||||
def __init__(self, coordinator, blind):
|
||||
"""Initialize the Motion Battery Sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._blind = blind
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the blind."""
|
||||
return f"{self._blind.mac}-battery"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info of the blind."""
|
||||
return {"identifiers": {(DOMAIN, self._blind.mac)}}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blind battery sensor."""
|
||||
return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return PERCENTAGE
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of this entity."""
|
||||
return DEVICE_CLASS_BATTERY
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._blind.battery_level
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage}
|
||||
|
||||
|
||||
class MotionTDBUBatterySensor(MotionBatterySensor):
|
||||
"""
|
||||
Representation of a Motion Battery Sensor for a Top Down Bottom Up blind.
|
||||
|
||||
Updates are done by the cover platform.
|
||||
"""
|
||||
|
||||
def __init__(self, coordinator, blind, motor):
|
||||
"""Initialize the Motion Battery Sensor."""
|
||||
super().__init__(coordinator, blind)
|
||||
|
||||
self._motor = motor
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the blind."""
|
||||
return f"{self._blind.mac}-{self._motor}-battery"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blind battery sensor."""
|
||||
return f"{self._blind.blind_type}-{self._motor}-battery-{self._blind.mac[12:]}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._blind.battery_level is None:
|
||||
return None
|
||||
return self._blind.battery_level[self._motor[0]]
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attributes = {}
|
||||
if self._blind.battery_voltage is not None:
|
||||
attributes[ATTR_BATTERY_VOLTAGE] = self._blind.battery_voltage[
|
||||
self._motor[0]
|
||||
]
|
||||
return attributes
|
||||
|
||||
|
||||
class MotionSignalStrengthSensor(CoordinatorEntity, Entity):
|
||||
"""Representation of a Motion Signal Strength Sensor."""
|
||||
|
||||
def __init__(self, coordinator, device, device_type):
|
||||
"""Initialize the Motion Signal Strength Sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._device = device
|
||||
self._device_type = device_type
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id of the blind."""
|
||||
return f"{self._device.mac}-RSSI"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device info of the blind."""
|
||||
return {"identifiers": {(DOMAIN, self._device.mac)}}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the blind signal strength sensor."""
|
||||
if self._device_type == TYPE_GATEWAY:
|
||||
return "Motion gateway signal strength"
|
||||
return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class of this entity."""
|
||||
return DEVICE_CLASS_SIGNAL_STRENGTH
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self):
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._device.RSSI
|
20
homeassistant/components/motion_blinds/strings.json
Normal file
20
homeassistant/components/motion_blinds/strings.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Motion Blinds",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Motion Blinds",
|
||||
"description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::ip%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"connection_error": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/motion_blinds/translations/en.json
Normal file
20
homeassistant/components/motion_blinds/translations/en.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Config flow for this Motion gateway is already in progress",
|
||||
"connection_error": "Failed to connect, please try again"
|
||||
},
|
||||
"flow_title": "Motion Blinds",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "IP address",
|
||||
"api_key": "API key"
|
||||
},
|
||||
"description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
|
||||
"title": "Motion Blinds"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -121,6 +121,7 @@ FLOWS = [
|
||||
"minecraft_server",
|
||||
"mobile_app",
|
||||
"monoprice",
|
||||
"motion_blinds",
|
||||
"mqtt",
|
||||
"myq",
|
||||
"neato",
|
||||
|
@ -951,6 +951,9 @@ minio==4.0.9
|
||||
# homeassistant.components.mitemp_bt
|
||||
mitemp_bt==0.0.3
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.1.6
|
||||
|
||||
# homeassistant.components.tts
|
||||
mutagen==1.45.1
|
||||
|
||||
|
@ -473,6 +473,9 @@ millheater==0.4.0
|
||||
# homeassistant.components.minio
|
||||
minio==4.0.9
|
||||
|
||||
# homeassistant.components.motion_blinds
|
||||
motionblinds==0.1.6
|
||||
|
||||
# homeassistant.components.tts
|
||||
mutagen==1.45.1
|
||||
|
||||
|
1
tests/components/motion_blinds/__init__.py
Normal file
1
tests/components/motion_blinds/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Motion Blinds integration."""
|
75
tests/components/motion_blinds/test_config_flow.py
Normal file
75
tests/components/motion_blinds/test_config_flow.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Test the Motion Blinds config flow."""
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME
|
||||
from homeassistant.components.motion_blinds.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
TEST_HOST = "1.2.3.4"
|
||||
TEST_API_KEY = "12ab345c-d67e-8f"
|
||||
|
||||
|
||||
@pytest.fixture(name="motion_blinds_connect", autouse=True)
|
||||
def motion_blinds_connect_fixture():
|
||||
"""Mock motion blinds connection and entry setup."""
|
||||
with patch(
|
||||
"homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.motion_blinds.gateway.MotionGateway.Update",
|
||||
return_value=True,
|
||||
), patch(
|
||||
"homeassistant.components.motion_blinds.async_setup_entry", return_value=True
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_config_flow_manual_host_success(hass):
|
||||
"""Successful flow manually initialized by the user."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == DEFAULT_GATEWAY_NAME
|
||||
assert result["data"] == {
|
||||
CONF_HOST: TEST_HOST,
|
||||
CONF_API_KEY: TEST_API_KEY,
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_connection_error(hass):
|
||||
"""Failed flow manually initialized by the user with connection timeout."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList",
|
||||
side_effect=socket.timeout,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "connection_error"
|
Reference in New Issue
Block a user