"""Support for Broadlink RM devices.""" from datetime import timedelta from ipaddress import ip_address import logging import broadlink as blk from broadlink.exceptions import BroadlinkException, CommandNotSupportedError import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON, ) from homeassistant.exceptions import PlatformNotReady 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, hostname, mac_address from .const import ( DEFAULT_NAME, DEFAULT_PORT, DEFAULT_TIMEOUT, MP1_TYPES, RM4_TYPES, RM_TYPES, SP1_TYPES, SP2_TYPES, ) from .device import BroadlinkDevice _LOGGER = logging.getLogger(__name__) TIME_BETWEEN_UPDATES = timedelta(seconds=5) CONF_SLOTS = "slots" CONF_RETRY = "retry" DEVICE_TYPES = RM_TYPES + RM4_TYPES + SP1_TYPES + SP2_TYPES + MP1_TYPES SWITCH_SCHEMA = vol.Schema( { vol.Optional(CONF_COMMAND_OFF): data_packet, vol.Optional(CONF_COMMAND_ON): data_packet, vol.Optional(CONF_FRIENDLY_NAME): cv.string, } ) MP1_SWITCH_SLOT_SCHEMA = vol.Schema( { vol.Optional("slot_1"): cv.string, vol.Optional("slot_2"): cv.string, vol.Optional("slot_3"): cv.string, vol.Optional("slot_4"): cv.string, } ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_SWITCHES, default={}): cv.schema_with_slug_keys( SWITCH_SCHEMA ), vol.Optional(CONF_SLOTS, default={}): MP1_SWITCH_SLOT_SCHEMA, 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=DEVICE_TYPES[0]): vol.In(DEVICE_TYPES), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Broadlink switches.""" host = config[CONF_HOST] mac_addr = config[CONF_MAC] friendly_name = config[CONF_FRIENDLY_NAME] model = config[CONF_TYPE] timeout = config[CONF_TIMEOUT] slots = config[CONF_SLOTS] devices = config[CONF_SWITCHES] 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), ) 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 model in RM_TYPES: api = blk.rm((host, DEFAULT_PORT), mac_addr, None) broadlink_device = BroadlinkDevice(hass, api) switches = generate_rm_switches(devices, broadlink_device) elif model in RM4_TYPES: api = blk.rm4((host, DEFAULT_PORT), mac_addr, None) broadlink_device = BroadlinkDevice(hass, api) switches = generate_rm_switches(devices, broadlink_device) elif model in SP1_TYPES: api = blk.sp1((host, DEFAULT_PORT), mac_addr, None) broadlink_device = BroadlinkDevice(hass, api) switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] elif model in SP2_TYPES: api = blk.sp2((host, DEFAULT_PORT), mac_addr, None) broadlink_device = BroadlinkDevice(hass, api) switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] elif model in MP1_TYPES: api = blk.mp1((host, DEFAULT_PORT), mac_addr, None) broadlink_device = BroadlinkDevice(hass, api) parent_device = BroadlinkMP1Switch(broadlink_device) switches = [ BroadlinkMP1Slot( get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device, ) for i in range(1, 5) ] api.timeout = timeout connected = await broadlink_device.async_connect() if not connected: raise PlatformNotReady if model in RM_TYPES or model in RM4_TYPES: hass.async_create_task(async_setup_service(hass, host, broadlink_device)) async_add_entities(switches) class BroadlinkRMSwitch(SwitchEntity, RestoreEntity): """Representation of an Broadlink switch.""" def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" self.device = device self.entity_id = f"{DOMAIN}.{slugify(name)}" self._name = friendly_name self._state = False self._command_on = command_on self._command_off = command_off async def async_added_to_hass(self): """Call when entity about to be added to hass.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state: self._state = state.state == STATE_ON @property def name(self): """Return the name of the switch.""" return self._name @property def assumed_state(self): """Return true if unable to access real state of entity.""" return True @property def available(self): """Return True if entity is available.""" return not self.should_poll or self.device.available @property def should_poll(self): """Return the polling state.""" return False @property def is_on(self): """Return true if device is on.""" return self._state async def async_update(self): """Update the state of the device.""" if not self.available: await self.device.async_connect() async def async_turn_on(self, **kwargs): """Turn the device on.""" if await self._async_send_packet(self._command_on): self._state = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" if await self._async_send_packet(self._command_off): self._state = False self.async_write_ha_state() async def _async_send_packet(self, packet): """Send packet to device.""" if packet is None: _LOGGER.debug("Empty packet") return True try: await self.device.async_request(self.device.api.send_data, packet) except BroadlinkException as err_msg: _LOGGER.error("Failed to send packet: %s", err_msg) return False return True class BroadlinkSP1Switch(BroadlinkRMSwitch): """Representation of an Broadlink switch.""" def __init__(self, friendly_name, device): """Initialize the switch.""" super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._load_power = None async def _async_send_packet(self, packet): """Send packet to device.""" try: await self.device.async_request(self.device.api.set_power, packet) except BroadlinkException as err_msg: _LOGGER.error("Failed to send packet: %s", err_msg) return False return True class BroadlinkSP2Switch(BroadlinkSP1Switch): """Representation of an Broadlink switch.""" @property def assumed_state(self): """Return true if unable to access real state of entity.""" return False @property def should_poll(self): """Return the polling state.""" return True @property def current_power_w(self): """Return the current power usage in Watt.""" try: return round(self._load_power, 2) except (ValueError, TypeError): return None async def async_update(self): """Update the state of the device.""" try: self._state = await self.device.async_request(self.device.api.check_power) except BroadlinkException as err_msg: _LOGGER.error("Failed to update state: %s", err_msg) return try: self._load_power = await self.device.async_request( self.device.api.get_energy ) except CommandNotSupportedError: return except BroadlinkException as err_msg: _LOGGER.error("Failed to update load power: %s", err_msg) class BroadlinkMP1Slot(BroadlinkRMSwitch): """Representation of a slot of Broadlink switch.""" def __init__(self, friendly_name, device, slot, parent_device): """Initialize the slot of switch.""" super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._slot = slot self._parent_device = parent_device @property def assumed_state(self): """Return true if unable to access real state of entity.""" return False @property def should_poll(self): """Return the polling state.""" return True async def async_update(self): """Update the state of the device.""" await self._parent_device.async_update() self._state = self._parent_device.get_outlet_status(self._slot) async def _async_send_packet(self, packet): """Send packet to device.""" try: await self.device.async_request( self.device.api.set_power, self._slot, packet ) except BroadlinkException as err_msg: _LOGGER.error("Failed to send packet: %s", err_msg) return False return True class BroadlinkMP1Switch: """Representation of a Broadlink switch - To fetch states of all slots.""" def __init__(self, device): """Initialize the switch.""" self.device = device self._states = None def get_outlet_status(self, slot): """Get status of outlet from cached status list.""" if self._states is None: return None return self._states[f"s{slot}"] @Throttle(TIME_BETWEEN_UPDATES) async def async_update(self): """Update the state of the device.""" try: states = await self.device.async_request(self.device.api.check_power) except BroadlinkException as err_msg: _LOGGER.error("Failed to update state: %s", err_msg) self._states = states