From a365f456fc47f609c61da0c7a87a1c86d302e0b1 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sat, 18 Apr 2020 20:16:49 -0300 Subject: [PATCH] Add support to the new Broadlink RM Mini 3 and RM4 Series (#32523) * Add device type * Use snake_case for devtype Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Validate device type as positive int * Add device type 0x5f36 to switch * Use default type for sensors * Add RM4 to switch platform * Use snake_case for devtype * Support multiple types of remote * Validate ip address * Improve code readability * Add const.py to .coveragerc * Use None for unknown device types * Fix sensors and standardize platform schemas * Fix if statement Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .coveragerc | 1 + homeassistant/components/broadlink/const.py | 31 ++++++ homeassistant/components/broadlink/remote.py | 28 +++-- homeassistant/components/broadlink/sensor.py | 65 +++++++----- homeassistant/components/broadlink/switch.py | 104 +++++++++---------- 5 files changed, 141 insertions(+), 88 deletions(-) diff --git a/.coveragerc b/.coveragerc index f898fbe7c77..28c7b1f15c6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -84,6 +84,7 @@ omit = homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py + homeassistant/components/broadlink/const.py homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py index c31a1540349..3264ec225ca 100644 --- a/homeassistant/components/broadlink/const.py +++ b/homeassistant/components/broadlink/const.py @@ -1,7 +1,38 @@ """Constants for broadlink platform.""" CONF_PACKET = "packet" +DEFAULT_LEARNING_TIMEOUT = 20 +DEFAULT_NAME = "Broadlink" +DEFAULT_PORT = 80 +DEFAULT_RETRY = 3 +DEFAULT_TIMEOUT = 5 + DOMAIN = "broadlink" SERVICE_LEARN = "learn" SERVICE_SEND = "send" + +A1_TYPES = ["a1"] +MP1_TYPES = ["mp1"] +RM_TYPES = [ + "rm", + "rm2", + "rm_mini", + "rm_mini_shate", + "rm_pro_phicomm", + "rm2_home_plus", + "rm2_home_plus_gdt", + "rm2_pro_plus", + "rm2_pro_plus2", + "rm2_pro_plus_bl", +] +RM4_TYPES = [ + "rm_mini3_newblackbean", + "rm_mini3_redbean", + "rm4_mini", + "rm4_pro", + "rm4c_mini", + "rm4c_pro", +] +SP1_TYPES = ["sp1"] +SP2_TYPES = ["sp2", "honeywell_sp2", "sp3", "spmini2", "spminiplus"] diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 769286a567e..177cf2ee0bd 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -8,7 +8,7 @@ from ipaddress import ip_address from itertools import product import logging -import broadlink +import broadlink as blk import voluptuous as vol from homeassistant.components.remote import ( @@ -24,7 +24,7 @@ from homeassistant.components.remote import ( SUPPORT_LEARN_COMMAND, RemoteDevice, ) -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -32,21 +32,26 @@ from homeassistant.helpers.storage import Store from homeassistant.util.dt import utcnow from . import DOMAIN, data_packet, hostname, mac_address +from .const import ( + DEFAULT_LEARNING_TIMEOUT, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_RETRY, + DEFAULT_TIMEOUT, + RM4_TYPES, + RM_TYPES, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_LEARNING_TIMEOUT = 20 -DEFAULT_NAME = "Broadlink" -DEFAULT_PORT = 80 -DEFAULT_RETRY = 3 -DEFAULT_TIMEOUT = 5 - SCAN_INTERVAL = timedelta(minutes=2) CODE_STORAGE_VERSION = 1 FLAG_STORAGE_VERSION = 1 FLAG_SAVE_DELAY = 15 +DEVICE_TYPES = RM_TYPES + RM4_TYPES + MINIMUM_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_COMMAND): vol.All( @@ -72,6 +77,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { 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.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } @@ -82,6 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Broadlink remote.""" host = config[CONF_HOST] mac_addr = config[CONF_MAC] + model = config[CONF_TYPE] timeout = config[CONF_TIMEOUT] name = config[CONF_NAME] unique_id = f"remote_{hexlify(mac_addr).decode('utf-8')}" @@ -91,7 +98,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return hass.data[DOMAIN][COMPONENT].append(unique_id) - api = broadlink.rm((host, DEFAULT_PORT), mac_addr, None) + if model in RM_TYPES: + api = blk.rm((host, DEFAULT_PORT), mac_addr, None) + else: + api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) api.timeout = timeout code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes") flag_storage = Store(hass, FLAG_STORAGE_VERSION, f"broadlink_{unique_id}_flags") diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index dbff4108a3f..13b1530eb6d 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -1,9 +1,9 @@ """Support for the Broadlink RM2 Pro (only temperature) and A1 devices.""" -import binascii from datetime import timedelta +from ipaddress import ip_address import logging -import broadlink +import broadlink as blk import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, CONF_TIMEOUT, + CONF_TYPE, TEMP_CELSIUS, UNIT_PERCENTAGE, ) @@ -21,10 +22,18 @@ import homeassistant.helpers.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_TIMEOUT, + RM4_TYPES, + RM_TYPES, +) + _LOGGER = logging.getLogger(__name__) -DEVICE_DEFAULT_NAME = "Broadlink sensor" -DEFAULT_TIMEOUT = 10 SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { @@ -35,14 +44,17 @@ SENSOR_TYPES = { "noise": ["Noise", " "], } +DEVICE_TYPES = A1_TYPES + RM_TYPES + RM4_TYPES + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), + 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): cv.string, - vol.Required(CONF_MAC): cv.string, + 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, } ) @@ -50,13 +62,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Broadlink device sensors.""" - host = config.get(CONF_HOST) - mac = config.get(CONF_MAC).encode().replace(b":", b"") - mac_addr = binascii.unhexlify(mac) - name = config.get(CONF_NAME) - timeout = config.get(CONF_TIMEOUT) + 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) - broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout) + + 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 + + api.timeout = timeout + broadlink_data = BroadlinkData(api, check_sensors, update_interval) dev = [] for variable in config[CONF_MONITORED_CONDITIONS]: dev.append(BroadlinkSensor(name, broadlink_data, variable)) @@ -109,13 +130,11 @@ class BroadlinkSensor(Entity): class BroadlinkData: """Representation of a Broadlink data object.""" - def __init__(self, interval, ip_addr, mac_addr, timeout): + def __init__(self, api, check_sensors, interval): """Initialize the data object.""" + self.api = api + self.check_sensors = check_sensors self.data = None - self.ip_addr = ip_addr - self.mac_addr = mac_addr - self.timeout = timeout - self._connect() self._schema = vol.Schema( { vol.Optional("temperature"): vol.Range(min=-50, max=150), @@ -129,14 +148,9 @@ class BroadlinkData: if not self._auth(): _LOGGER.warning("Failed to connect to device") - def _connect(self): - - self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None) - self._device.timeout = self.timeout - def _update(self, retry=3): try: - data = self._device.check_sensors_raw() + data = self.check_sensors() if data is not None: self.data = self._schema(data) return @@ -152,10 +166,9 @@ class BroadlinkData: def _auth(self, retry=3): try: - auth = self._device.auth() + auth = self.api.auth() except OSError: auth = False if not auth and retry > 0: - self._connect() return self._auth(retry - 1) return auth diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 9b986ae75d4..6eed0646d76 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -1,10 +1,10 @@ """Support for Broadlink RM devices.""" -import binascii from datetime import timedelta +from ipaddress import ip_address import logging import socket -import broadlink +import broadlink as blk import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice @@ -23,35 +23,27 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import Throttle, slugify -from . import async_setup_service, data_packet +from . import async_setup_service, data_packet, hostname, mac_address +from .const import ( + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_RETRY, + DEFAULT_TIMEOUT, + MP1_TYPES, + RM4_TYPES, + RM_TYPES, + SP1_TYPES, + SP2_TYPES, +) _LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = timedelta(seconds=5) -DEFAULT_NAME = "Broadlink switch" -DEFAULT_TIMEOUT = 10 -DEFAULT_RETRY = 2 CONF_SLOTS = "slots" CONF_RETRY = "retry" -RM_TYPES = [ - "rm", - "rm2", - "rm_mini", - "rm_pro_phicomm", - "rm2_home_plus", - "rm2_home_plus_gdt", - "rm2_pro_plus", - "rm2_pro_plus2", - "rm2_pro_plus_bl", - "rm_mini_shate", -] -SP1_TYPES = ["sp1"] -SP2_TYPES = ["sp2", "honeywell_sp2", "sp3", "spmini2", "spminiplus"] -MP1_TYPES = ["mp1"] - -SWITCH_TYPES = RM_TYPES + SP1_TYPES + SP2_TYPES + MP1_TYPES +DEVICE_TYPES = RM_TYPES + RM4_TYPES + SP1_TYPES + SP2_TYPES + MP1_TYPES SWITCH_SCHEMA = vol.Schema( { @@ -76,10 +68,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( SWITCH_SCHEMA ), vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MAC): cv.string, + vol.Required(CONF_HOST): vol.All(vol.Any(hostname, ip_address), cv.string), + vol.Required(CONF_MAC): mac_address, vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TYPE, default=SWITCH_TYPES[0]): vol.In(SWITCH_TYPES), + vol.Optional(CONF_TYPE, default=DEVICE_TYPES[0]): vol.In(DEVICE_TYPES), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, } @@ -91,47 +83,53 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = config.get(CONF_SWITCHES) slots = config.get("slots", {}) - ip_addr = config.get(CONF_HOST) + host = config.get(CONF_HOST) + mac_addr = config.get(CONF_MAC) friendly_name = config.get(CONF_FRIENDLY_NAME) - mac_addr = binascii.unhexlify(config.get(CONF_MAC).encode().replace(b":", b"")) - switch_type = config.get(CONF_TYPE) + model = config[CONF_TYPE] retry_times = config.get(CONF_RETRY) - def _get_mp1_slot_name(switch_friendly_name, slot): + def generate_rm_switches(switches, broadlink_device): + """Generate RM switches.""" + return [ + BroadlinkRMSwitch( + object_id, + config.get(CONF_FRIENDLY_NAME, object_id), + broadlink_device, + config.get(CONF_COMMAND_ON), + config.get(CONF_COMMAND_OFF), + retry_times, + ) + for object_id, config in switches.items() + ] + + def get_mp1_slot_name(switch_friendly_name, slot): """Get slot name.""" if not slots[f"slot_{slot}"]: return f"{switch_friendly_name} slot {slot}" return slots[f"slot_{slot}"] - if switch_type in RM_TYPES: - broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) - hass.add_job(async_setup_service, hass, ip_addr, broadlink_device) - - switches = [] - for object_id, device_config in devices.items(): - switches.append( - BroadlinkRMSwitch( - object_id, - device_config.get(CONF_FRIENDLY_NAME, object_id), - broadlink_device, - device_config.get(CONF_COMMAND_ON), - device_config.get(CONF_COMMAND_OFF), - retry_times, - ) - ) - elif switch_type in SP1_TYPES: - broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr, None) + if model in RM_TYPES: + broadlink_device = blk.rm((host, DEFAULT_PORT), mac_addr, None) + hass.add_job(async_setup_service, hass, host, broadlink_device) + switches = generate_rm_switches(devices, broadlink_device) + elif model in RM4_TYPES: + broadlink_device = blk.rm4((host, DEFAULT_PORT), mac_addr, None) + hass.add_job(async_setup_service, hass, host, broadlink_device) + switches = generate_rm_switches(devices, broadlink_device) + elif model in SP1_TYPES: + broadlink_device = blk.sp1((host, DEFAULT_PORT), mac_addr, None) switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] - elif switch_type in SP2_TYPES: - broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None) + elif model in SP2_TYPES: + broadlink_device = blk.sp2((host, DEFAULT_PORT), mac_addr, None) switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] - elif switch_type in MP1_TYPES: + elif model in MP1_TYPES: switches = [] - broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr, None) + broadlink_device = blk.mp1((host, DEFAULT_PORT), mac_addr, None) parent_device = BroadlinkMP1Switch(broadlink_device, retry_times) for i in range(1, 5): slot = BroadlinkMP1Slot( - _get_mp1_slot_name(friendly_name, i), + get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device,