"""Support for Broadlink switches.""" from abc import ABC, abstractmethod from functools import partial import logging from broadlink.exceptions import BroadlinkException import voluptuous as vol from homeassistant.components.switch import ( DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, PLATFORM_SCHEMA, SwitchEntity, ) from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_NAME, CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE, STATE_ON, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from .const import DOMAIN, SWITCH_DOMAIN from .helpers import data_packet, import_device, mac_address _LOGGER = logging.getLogger(__name__) CONF_SLOTS = "slots" SWITCH_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_COMMAND_OFF): data_packet, vol.Optional(CONF_COMMAND_ON): data_packet, } ) OLD_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, } ) PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HOST), cv.deprecated(CONF_SLOTS), cv.deprecated(CONF_TIMEOUT), cv.deprecated(CONF_TYPE), PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): mac_address, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_SWITCHES, default=[]): vol.Any( cv.schema_with_slug_keys(OLD_SWITCH_SCHEMA), vol.All(cv.ensure_list, [SWITCH_SCHEMA]), ), } ), ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Import the device and set up custom switches. This is for backward compatibility. Do not use this method. """ mac_addr = config[CONF_MAC] host = config.get(CONF_HOST) switches = config.get(CONF_SWITCHES) if not isinstance(switches, list): switches = [ {CONF_NAME: switch.pop(CONF_FRIENDLY_NAME, name), **switch} for name, switch in switches.items() ] _LOGGER.warning( "Your configuration for the switch platform is deprecated. " "Please refer to the Broadlink documentation to catch up" ) if switches: platform_data = hass.data[DOMAIN].platforms.setdefault(SWITCH_DOMAIN, {}) platform_data.setdefault(mac_addr, []).extend(switches) else: _LOGGER.warning( "The switch platform is deprecated, except for custom IR/RF " "switches. Please refer to the Broadlink documentation to " "catch up" ) if host: import_device(hass, host) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Broadlink switch.""" device = hass.data[DOMAIN].devices[config_entry.entry_id] if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: platform_data = hass.data[DOMAIN].platforms.get(SWITCH_DOMAIN, {}) user_defined_switches = platform_data.get(device.api.mac, {}) switches = [ BroadlinkRMSwitch(device, config) for config in user_defined_switches ] elif device.api.type == "SP1": switches = [BroadlinkSP1Switch(device)] elif device.api.type in {"SP2", "SP2S", "SP3", "SP3S", "SP4", "SP4B"}: switches = [BroadlinkSP2Switch(device)] elif device.api.type == "BG1": switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)] elif device.api.type == "MP1": switches = [BroadlinkMP1Slot(device, slot) for slot in range(1, 5)] async_add_entities(switches) class BroadlinkSwitch(SwitchEntity, RestoreEntity, ABC): """Representation of a Broadlink switch.""" def __init__(self, device, command_on, command_off): """Initialize the switch.""" self._device = device self._command_on = command_on self._command_off = command_off self._coordinator = device.update_manager.coordinator self._state = None @property def name(self): """Return the name of the switch.""" return f"{self._device.name} Switch" @property def assumed_state(self): """Return True if unable to access real state of the switch.""" return True @property def available(self): """Return True if the switch is available.""" return self._device.update_manager.available @property def is_on(self): """Return True if the switch is on.""" return self._state @property def should_poll(self): """Return True if the switch has to be polled for state.""" return False @property def device_class(self): """Return device class.""" return DEVICE_CLASS_SWITCH @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.""" self.async_write_ha_state() async def async_added_to_hass(self): """Call when the switch is added to hass.""" if self._state is None: state = await self.async_get_last_state() self._state = state is not None and state.state == STATE_ON self.async_on_remove(self._coordinator.async_add_listener(self.update_data)) async def async_update(self): """Update the switch.""" await self._coordinator.async_request_refresh() async def async_turn_on(self, **kwargs): """Turn on the switch.""" 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 off the switch.""" if await self._async_send_packet(self._command_off): self._state = False self.async_write_ha_state() @abstractmethod async def _async_send_packet(self, packet): """Send a packet to the device.""" class BroadlinkRMSwitch(BroadlinkSwitch): """Representation of a Broadlink RM switch.""" def __init__(self, device, config): """Initialize the switch.""" super().__init__( device, config.get(CONF_COMMAND_ON), config.get(CONF_COMMAND_OFF) ) self._name = config[CONF_NAME] @property def name(self): """Return the name of the switch.""" return self._name async def _async_send_packet(self, packet): """Send a packet to the device.""" if packet is None: return True try: await self._device.async_request(self._device.api.send_data, packet) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False return True class BroadlinkSP1Switch(BroadlinkSwitch): """Representation of a Broadlink SP1 switch.""" def __init__(self, device): """Initialize the switch.""" super().__init__(device, 1, 0) @property def unique_id(self): """Return the unique id of the switch.""" return self._device.unique_id async def _async_send_packet(self, packet): """Send a packet to the device.""" try: await self._device.async_request(self._device.api.set_power, packet) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False return True class BroadlinkSP2Switch(BroadlinkSP1Switch): """Representation of a Broadlink SP2 switch.""" def __init__(self, device, *args, **kwargs): """Initialize the switch.""" super().__init__(device, *args, **kwargs) self._state = self._coordinator.data["pwr"] self._load_power = self._coordinator.data.get("power") @property def assumed_state(self): """Return True if unable to access real state of the switch.""" return False @property def current_power_w(self): """Return the current power usage in Watt.""" return self._load_power @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: self._state = self._coordinator.data["pwr"] self._load_power = self._coordinator.data.get("power") self.async_write_ha_state() class BroadlinkMP1Slot(BroadlinkSwitch): """Representation of a Broadlink MP1 slot.""" def __init__(self, device, slot): """Initialize the switch.""" super().__init__(device, 1, 0) self._slot = slot self._state = self._coordinator.data[f"s{slot}"] @property def unique_id(self): """Return the unique id of the slot.""" return f"{self._device.unique_id}-s{self._slot}" @property def name(self): """Return the name of the switch.""" return f"{self._device.name} S{self._slot}" @property def assumed_state(self): """Return True if unable to access real state of the switch.""" return False @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: self._state = self._coordinator.data[f"s{self._slot}"] self.async_write_ha_state() async def _async_send_packet(self, packet): """Send a packet to the device.""" try: await self._device.async_request( self._device.api.set_power, self._slot, packet ) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False return True class BroadlinkBG1Slot(BroadlinkSwitch): """Representation of a Broadlink BG1 slot.""" def __init__(self, device, slot): """Initialize the switch.""" super().__init__(device, 1, 0) self._slot = slot self._state = self._coordinator.data[f"pwr{slot}"] @property def unique_id(self): """Return the unique id of the slot.""" return f"{self._device.unique_id}-s{self._slot}" @property def name(self): """Return the name of the switch.""" return f"{self._device.name} S{self._slot}" @property def assumed_state(self): """Return True if unable to access real state of the switch.""" return False @property def device_class(self): """Return device class.""" return DEVICE_CLASS_OUTLET @callback def update_data(self): """Update data.""" if self._coordinator.last_update_success: self._state = self._coordinator.data[f"pwr{self._slot}"] self.async_write_ha_state() async def _async_send_packet(self, packet): """Send a packet to the device.""" set_state = partial(self._device.api.set_state, **{f"pwr{self._slot}": packet}) try: await self._device.async_request(set_state) except (BroadlinkException, OSError) as err: _LOGGER.error("Failed to send packet: %s", err) return False return True