core/homeassistant/components/broadlink/switch.py

342 lines
11 KiB
Python

"""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