Implement config flow in the Broadlink integration (#36914)

* Implement config flow in the Broadlink integration

* General improvements to the Broadlink config flow

* Remove unnecessary else after return

* Fix translations

* Rename device to device_entry

* Add tests for the config flow

* Improve docstrings

* Test we do not accept more than one config entry per device

* Improve helpers

* Allow empty packets

* Allow multiple config files for switches related to the same device

* Rename mock_device to mock_api

* General improvements

* Make new attempts before marking the device as unavailable

* Let the name be the template for the entity_id

* Handle OSError

* Test network unavailable in the configuration flow

* Rename lock attribute

* Update manifest.json

* Import devices from platforms

* Test import flow

* Add deprecation warnings

* General improvements

* Rename deprecate to discontinue

* Test device setup

* Add type attribute to mock api

* Test we handle an update failure at startup

* Remove BroadlinkDevice from tests

* Remove device.py from .coveragerc

* Add tests for the config flow

* Add tests for the device

* Test device registry and update listener

* Test MAC address validation

* Add tests for the device

* Extract domains and types to a helper function

* Do not patch integration details

* Add tests for the device

* Set device classes where appropriate

* Set an appropriate connection class

* Do not set device class for custom switches

* Fix tests and improve code readability

* Use RM4 to test authentication errors

* Handle BroadlinkException in the authentication
This commit is contained in:
Felipe Martins Diel 2020-08-20 12:30:41 -03:00 committed by GitHub
parent eb4f667a1a
commit a2c1f08c8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 2497 additions and 795 deletions

View file

@ -1,115 +1,80 @@
"""Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
from datetime import timedelta
from ipaddress import ip_address
"""Support for Broadlink sensors."""
import logging
import broadlink as blk
from broadlink.exceptions import BroadlinkException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
CONF_MONITORED_CONDITIONS,
CONF_NAME,
CONF_SCAN_INTERVAL,
CONF_TIMEOUT,
CONF_TYPE,
TEMP_CELSIUS,
UNIT_PERCENTAGE,
from homeassistant.components.sensor import (
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
PLATFORM_SCHEMA,
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, UNIT_PERCENTAGE
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from . import hostname, mac_address
from .const import (
A1_TYPES,
DEFAULT_NAME,
DEFAULT_PORT,
DEFAULT_RETRY,
DEFAULT_TIMEOUT,
RM4_TYPES,
RM_TYPES,
)
from .device import BroadlinkDevice
from .const import DOMAIN
from .helpers import import_device
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=300)
SENSOR_TYPES = {
"temperature": ["Temperature", TEMP_CELSIUS],
"air_quality": ["Air Quality", " "],
"humidity": ["Humidity", UNIT_PERCENTAGE],
"light": ["Light", " "],
"noise": ["Noise", " "],
"temperature": ("Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE),
"air_quality": ("Air Quality", None, None),
"humidity": ("Humidity", UNIT_PERCENTAGE, DEVICE_CLASS_HUMIDITY),
"light": ("Light", None, DEVICE_CLASS_ILLUMINANCE),
"noise": ("Noise", None, None),
}
DEVICE_TYPES = A1_TYPES + RM_TYPES + RM4_TYPES
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAME, default=DEFAULT_NAME): vol.Coerce(str),
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
),
vol.Required(CONF_HOST): vol.All(vol.Any(hostname, ip_address), cv.string),
vol.Required(CONF_MAC): mac_address,
vol.Optional(CONF_TYPE, default=DEVICE_TYPES[0]): vol.In(DEVICE_TYPES),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}
{vol.Required(CONF_HOST): cv.string}, extra=vol.ALLOW_EXTRA
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Broadlink device sensors."""
host = config[CONF_HOST]
mac_addr = config[CONF_MAC]
model = config[CONF_TYPE]
name = config[CONF_NAME]
timeout = config[CONF_TIMEOUT]
update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
"""Import the device and discontinue platform.
if model in RM4_TYPES:
api = blk.rm4((host, DEFAULT_PORT), mac_addr, None)
check_sensors = api.check_sensors
else:
api = blk.a1((host, DEFAULT_PORT), mac_addr, None)
check_sensors = api.check_sensors_raw
This is for backward compatibility.
Do not use this method.
"""
import_device(hass, config[CONF_HOST])
_LOGGER.warning(
"The sensor platform is deprecated, please remove it from your configuration"
)
api.timeout = timeout
device = BroadlinkDevice(hass, api)
connected = await device.async_connect()
if not connected:
raise PlatformNotReady
broadlink_data = BroadlinkData(device, check_sensors, update_interval)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Broadlink sensor."""
device = hass.data[DOMAIN].devices[config_entry.entry_id]
sensor_data = device.update_manager.coordinator.data
sensors = [
BroadlinkSensor(name, broadlink_data, variable)
for variable in config[CONF_MONITORED_CONDITIONS]
BroadlinkSensor(device, monitored_condition)
for monitored_condition in sensor_data
if sensor_data[monitored_condition] or device.api.type == "A1"
]
async_add_entities(sensors, True)
async_add_entities(sensors)
class BroadlinkSensor(Entity):
"""Representation of a Broadlink device sensor."""
"""Representation of a Broadlink sensor."""
def __init__(self, name, broadlink_data, sensor_type):
def __init__(self, device, monitored_condition):
"""Initialize the sensor."""
self._name = f"{name} {SENSOR_TYPES[sensor_type][0]}"
self._state = None
self._type = sensor_type
self._broadlink_data = broadlink_data
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._device = device
self._coordinator = device.update_manager.coordinator
self._monitored_condition = monitored_condition
self._state = self._coordinator.data[monitored_condition]
@property
def unique_id(self):
"""Return the unique id of the sensor."""
return f"{self._device.unique_id}-{self._monitored_condition}"
@property
def name(self):
"""Return the name of the sensor."""
return self._name
return f"{self._device.name} {SENSOR_TYPES[self._monitored_condition][0]}"
@property
def state(self):
@ -118,52 +83,46 @@ class BroadlinkSensor(Entity):
@property
def available(self):
"""Return True if entity is available."""
return self._broadlink_data.device.available
"""Return True if the sensor is available."""
return self._device.update_manager.available
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return self._unit_of_measurement
"""Return the unit of measurement of the sensor."""
return SENSOR_TYPES[self._monitored_condition][1]
@property
def should_poll(self):
"""Return True if the sensor has to be polled for state."""
return False
@property
def device_class(self):
"""Return device class."""
return SENSOR_TYPES[self._monitored_condition][2]
@property
def device_info(self):
"""Return device info."""
return {
"identifiers": {(DOMAIN, self._device.unique_id)},
"manufacturer": self._device.api.manufacturer,
"model": self._device.api.model,
"name": self._device.name,
"sw_version": self._device.fw_version,
}
@callback
def update_data(self):
"""Update data."""
if self._coordinator.last_update_success:
self._state = self._coordinator.data[self._monitored_condition]
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Call when the sensor is added to hass."""
self.async_on_remove(self._coordinator.async_add_listener(self.update_data))
async def async_update(self):
"""Get the latest data from the sensor."""
await self._broadlink_data.async_update()
self._state = self._broadlink_data.data.get(self._type)
class BroadlinkData:
"""Representation of a Broadlink data object."""
def __init__(self, device, check_sensors, interval):
"""Initialize the data object."""
self.device = device
self.check_sensors = check_sensors
self.data = {}
self._schema = vol.Schema(
{
vol.Optional("temperature"): vol.Range(min=-50, max=150),
vol.Optional("humidity"): vol.Range(min=0, max=100),
vol.Optional("light"): vol.Any(0, 1, 2, 3),
vol.Optional("air_quality"): vol.Any(0, 1, 2, 3),
vol.Optional("noise"): vol.Any(0, 1, 2),
}
)
self.async_update = Throttle(interval)(self._async_fetch_data)
async def _async_fetch_data(self):
"""Fetch sensor data."""
for _ in range(DEFAULT_RETRY):
try:
data = await self.device.async_request(self.check_sensors)
except BroadlinkException:
return
try:
data = self._schema(data)
except (vol.Invalid, vol.MultipleInvalid):
continue
else:
self.data = data
return
_LOGGER.debug("Failed to update sensors: Device returned malformed data")
"""Update the sensor."""
await self._coordinator.async_request_refresh()