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:
parent
eb4f667a1a
commit
a2c1f08c8c
21 changed files with 2497 additions and 795 deletions
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue