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>
This commit is contained in:
Felipe Martins Diel 2020-04-18 20:16:49 -03:00 committed by GitHub
parent 37463d655a
commit a365f456fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 88 deletions

View File

@ -84,6 +84,7 @@ omit =
homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/__init__.py
homeassistant/components/braviatv/const.py homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/media_player.py
homeassistant/components/broadlink/const.py
homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/remote.py
homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/sensor.py
homeassistant/components/broadlink/switch.py homeassistant/components/broadlink/switch.py

View File

@ -1,7 +1,38 @@
"""Constants for broadlink platform.""" """Constants for broadlink platform."""
CONF_PACKET = "packet" CONF_PACKET = "packet"
DEFAULT_LEARNING_TIMEOUT = 20
DEFAULT_NAME = "Broadlink"
DEFAULT_PORT = 80
DEFAULT_RETRY = 3
DEFAULT_TIMEOUT = 5
DOMAIN = "broadlink" DOMAIN = "broadlink"
SERVICE_LEARN = "learn" SERVICE_LEARN = "learn"
SERVICE_SEND = "send" 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"]

View File

@ -8,7 +8,7 @@ from ipaddress import ip_address
from itertools import product from itertools import product
import logging import logging
import broadlink import broadlink as blk
import voluptuous as vol import voluptuous as vol
from homeassistant.components.remote import ( from homeassistant.components.remote import (
@ -24,7 +24,7 @@ from homeassistant.components.remote import (
SUPPORT_LEARN_COMMAND, SUPPORT_LEARN_COMMAND,
RemoteDevice, 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.core import callback
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -32,21 +32,26 @@ from homeassistant.helpers.storage import Store
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import DOMAIN, data_packet, hostname, mac_address 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__) _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) SCAN_INTERVAL = timedelta(minutes=2)
CODE_STORAGE_VERSION = 1 CODE_STORAGE_VERSION = 1
FLAG_STORAGE_VERSION = 1 FLAG_STORAGE_VERSION = 1
FLAG_SAVE_DELAY = 15 FLAG_SAVE_DELAY = 15
DEVICE_TYPES = RM_TYPES + RM4_TYPES
MINIMUM_SERVICE_SCHEMA = vol.Schema( MINIMUM_SERVICE_SCHEMA = vol.Schema(
{ {
vol.Required(ATTR_COMMAND): vol.All( 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_HOST): vol.All(vol.Any(hostname, ip_address), cv.string),
vol.Required(CONF_MAC): mac_address, 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_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 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.""" """Set up the Broadlink remote."""
host = config[CONF_HOST] host = config[CONF_HOST]
mac_addr = config[CONF_MAC] mac_addr = config[CONF_MAC]
model = config[CONF_TYPE]
timeout = config[CONF_TIMEOUT] timeout = config[CONF_TIMEOUT]
name = config[CONF_NAME] name = config[CONF_NAME]
unique_id = f"remote_{hexlify(mac_addr).decode('utf-8')}" 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 return
hass.data[DOMAIN][COMPONENT].append(unique_id) 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 api.timeout = timeout
code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes") code_storage = Store(hass, CODE_STORAGE_VERSION, f"broadlink_{unique_id}_codes")
flag_storage = Store(hass, FLAG_STORAGE_VERSION, f"broadlink_{unique_id}_flags") flag_storage = Store(hass, FLAG_STORAGE_VERSION, f"broadlink_{unique_id}_flags")

View File

@ -1,9 +1,9 @@
"""Support for the Broadlink RM2 Pro (only temperature) and A1 devices.""" """Support for the Broadlink RM2 Pro (only temperature) and A1 devices."""
import binascii
from datetime import timedelta from datetime import timedelta
from ipaddress import ip_address
import logging import logging
import broadlink import broadlink as blk
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_TIMEOUT, CONF_TIMEOUT,
CONF_TYPE,
TEMP_CELSIUS, TEMP_CELSIUS,
UNIT_PERCENTAGE, UNIT_PERCENTAGE,
) )
@ -21,10 +22,18 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle 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__) _LOGGER = logging.getLogger(__name__)
DEVICE_DEFAULT_NAME = "Broadlink sensor"
DEFAULT_TIMEOUT = 10
SCAN_INTERVAL = timedelta(seconds=300) SCAN_INTERVAL = timedelta(seconds=300)
SENSOR_TYPES = { SENSOR_TYPES = {
@ -35,14 +44,17 @@ SENSOR_TYPES = {
"noise": ["Noise", " "], "noise": ["Noise", " "],
} }
DEVICE_TYPES = A1_TYPES + RM_TYPES + RM4_TYPES
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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( vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)] cv.ensure_list, [vol.In(SENSOR_TYPES)]
), ),
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): vol.All(vol.Any(hostname, ip_address), cv.string),
vol.Required(CONF_MAC): 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_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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Broadlink device sensors.""" """Set up the Broadlink device sensors."""
host = config.get(CONF_HOST) host = config[CONF_HOST]
mac = config.get(CONF_MAC).encode().replace(b":", b"") mac_addr = config[CONF_MAC]
mac_addr = binascii.unhexlify(mac) model = config[CONF_TYPE]
name = config.get(CONF_NAME) name = config[CONF_NAME]
timeout = config.get(CONF_TIMEOUT) timeout = config[CONF_TIMEOUT]
update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) 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 = [] dev = []
for variable in config[CONF_MONITORED_CONDITIONS]: for variable in config[CONF_MONITORED_CONDITIONS]:
dev.append(BroadlinkSensor(name, broadlink_data, variable)) dev.append(BroadlinkSensor(name, broadlink_data, variable))
@ -109,13 +130,11 @@ class BroadlinkSensor(Entity):
class BroadlinkData: class BroadlinkData:
"""Representation of a Broadlink data object.""" """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.""" """Initialize the data object."""
self.api = api
self.check_sensors = check_sensors
self.data = None self.data = None
self.ip_addr = ip_addr
self.mac_addr = mac_addr
self.timeout = timeout
self._connect()
self._schema = vol.Schema( self._schema = vol.Schema(
{ {
vol.Optional("temperature"): vol.Range(min=-50, max=150), vol.Optional("temperature"): vol.Range(min=-50, max=150),
@ -129,14 +148,9 @@ class BroadlinkData:
if not self._auth(): if not self._auth():
_LOGGER.warning("Failed to connect to device") _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): def _update(self, retry=3):
try: try:
data = self._device.check_sensors_raw() data = self.check_sensors()
if data is not None: if data is not None:
self.data = self._schema(data) self.data = self._schema(data)
return return
@ -152,10 +166,9 @@ class BroadlinkData:
def _auth(self, retry=3): def _auth(self, retry=3):
try: try:
auth = self._device.auth() auth = self.api.auth()
except OSError: except OSError:
auth = False auth = False
if not auth and retry > 0: if not auth and retry > 0:
self._connect()
return self._auth(retry - 1) return self._auth(retry - 1)
return auth return auth

View File

@ -1,10 +1,10 @@
"""Support for Broadlink RM devices.""" """Support for Broadlink RM devices."""
import binascii
from datetime import timedelta from datetime import timedelta
from ipaddress import ip_address
import logging import logging
import socket import socket
import broadlink import broadlink as blk
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice 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.helpers.restore_state import RestoreEntity
from homeassistant.util import Throttle, slugify 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__) _LOGGER = logging.getLogger(__name__)
TIME_BETWEEN_UPDATES = timedelta(seconds=5) TIME_BETWEEN_UPDATES = timedelta(seconds=5)
DEFAULT_NAME = "Broadlink switch"
DEFAULT_TIMEOUT = 10
DEFAULT_RETRY = 2
CONF_SLOTS = "slots" CONF_SLOTS = "slots"
CONF_RETRY = "retry" CONF_RETRY = "retry"
RM_TYPES = [ DEVICE_TYPES = RM_TYPES + RM4_TYPES + SP1_TYPES + SP2_TYPES + MP1_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
SWITCH_SCHEMA = vol.Schema( SWITCH_SCHEMA = vol.Schema(
{ {
@ -76,10 +68,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
SWITCH_SCHEMA SWITCH_SCHEMA
), ),
vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA,
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): vol.All(vol.Any(hostname, ip_address), cv.string),
vol.Required(CONF_MAC): cv.string, vol.Required(CONF_MAC): mac_address,
vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, 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_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): 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) devices = config.get(CONF_SWITCHES)
slots = config.get("slots", {}) 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) friendly_name = config.get(CONF_FRIENDLY_NAME)
mac_addr = binascii.unhexlify(config.get(CONF_MAC).encode().replace(b":", b"")) model = config[CONF_TYPE]
switch_type = config.get(CONF_TYPE)
retry_times = config.get(CONF_RETRY) 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.""" """Get slot name."""
if not slots[f"slot_{slot}"]: if not slots[f"slot_{slot}"]:
return f"{switch_friendly_name} slot {slot}" return f"{switch_friendly_name} slot {slot}"
return slots[f"slot_{slot}"] return slots[f"slot_{slot}"]
if switch_type in RM_TYPES: if model in RM_TYPES:
broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) broadlink_device = blk.rm((host, DEFAULT_PORT), mac_addr, None)
hass.add_job(async_setup_service, hass, ip_addr, broadlink_device) hass.add_job(async_setup_service, hass, host, broadlink_device)
switches = generate_rm_switches(devices, broadlink_device)
switches = [] elif model in RM4_TYPES:
for object_id, device_config in devices.items(): broadlink_device = blk.rm4((host, DEFAULT_PORT), mac_addr, None)
switches.append( hass.add_job(async_setup_service, hass, host, broadlink_device)
BroadlinkRMSwitch( switches = generate_rm_switches(devices, broadlink_device)
object_id, elif model in SP1_TYPES:
device_config.get(CONF_FRIENDLY_NAME, object_id), broadlink_device = blk.sp1((host, DEFAULT_PORT), mac_addr, None)
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)
switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)]
elif switch_type in SP2_TYPES: elif model in SP2_TYPES:
broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None) broadlink_device = blk.sp2((host, DEFAULT_PORT), mac_addr, None)
switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)]
elif switch_type in MP1_TYPES: elif model in MP1_TYPES:
switches = [] 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) parent_device = BroadlinkMP1Switch(broadlink_device, retry_times)
for i in range(1, 5): for i in range(1, 5):
slot = BroadlinkMP1Slot( slot = BroadlinkMP1Slot(
_get_mp1_slot_name(friendly_name, i), get_mp1_slot_name(friendly_name, i),
broadlink_device, broadlink_device,
i, i,
parent_device, parent_device,