From 5ed1f16f250e9c52c53c6f5931e2a56bf57ec488 Mon Sep 17 00:00:00 2001 From: Trekky12 Date: Tue, 7 Jan 2020 02:13:08 +0100 Subject: [PATCH] Add pilight dimmer as light component (#30107) * Add pilight dimmer as light component * fix CI errors * fix missing new lines * improve formatting and addresses comments of @springstan * rename config parameter and remove super() call to match pylint * import only used constants of the pilight component * Add myself to the code owners * fix CODEOWNERS --- CODEOWNERS | 1 + .../components/pilight/base_class.py | 185 ++++++++++++++++++ homeassistant/components/pilight/const.py | 14 ++ homeassistant/components/pilight/light.py | 67 +++++++ .../components/pilight/manifest.json | 2 +- homeassistant/components/pilight/switch.py | 185 +----------------- 6 files changed, 274 insertions(+), 180 deletions(-) create mode 100644 homeassistant/components/pilight/base_class.py create mode 100644 homeassistant/components/pilight/const.py create mode 100644 homeassistant/components/pilight/light.py diff --git a/CODEOWNERS b/CODEOWNERS index b9db06f8f56..93b41c6d38c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -252,6 +252,7 @@ homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff @johnluetke +homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren diff --git a/homeassistant/components/pilight/base_class.py b/homeassistant/components/pilight/base_class.py new file mode 100644 index 00000000000..382d47f41cd --- /dev/null +++ b/homeassistant/components/pilight/base_class.py @@ -0,0 +1,185 @@ +"""Base class for pilight.""" +import logging + +import voluptuous as vol + +from homeassistant.components.pilight import DOMAIN, EVENT, SERVICE_NAME +from homeassistant.const import ( + CONF_ID, + CONF_NAME, + CONF_PROTOCOL, + CONF_STATE, + STATE_OFF, + STATE_ON, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + CONF_ECHO, + CONF_OFF, + CONF_OFF_CODE, + CONF_OFF_CODE_RECEIVE, + CONF_ON, + CONF_ON_CODE, + CONF_ON_CODE_RECEIVE, + CONF_SYSTEMCODE, + CONF_UNIT, + CONF_UNITCODE, +) + +_LOGGER = logging.getLogger(__name__) + +COMMAND_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PROTOCOL): cv.string, + vol.Optional(CONF_ON): cv.positive_int, + vol.Optional(CONF_OFF): cv.positive_int, + vol.Optional(CONF_UNIT): cv.positive_int, + vol.Optional(CONF_UNITCODE): cv.positive_int, + vol.Optional(CONF_ID): vol.Any(cv.positive_int, cv.string), + vol.Optional(CONF_STATE): vol.Any(STATE_ON, STATE_OFF), + vol.Optional(CONF_SYSTEMCODE): cv.positive_int, + }, + extra=vol.ALLOW_EXTRA, +) + +RECEIVE_SCHEMA = COMMAND_SCHEMA.extend({vol.Optional(CONF_ECHO): cv.boolean}) + +SWITCHES_SCHEMA = vol.Schema( + { + vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, + vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OFF_CODE_RECEIVE): vol.All(cv.ensure_list, [COMMAND_SCHEMA]), + vol.Optional(CONF_ON_CODE_RECEIVE): vol.All(cv.ensure_list, [COMMAND_SCHEMA]), + } +) + + +class PilightBaseDevice(RestoreEntity): + """Base class for pilight switches and lights.""" + + def __init__(self, hass, name, config): + """Initialize a device.""" + self._hass = hass + self._name = config.get(CONF_NAME, name) + self._is_on = False + self._code_on = config.get(CONF_ON_CODE) + self._code_off = config.get(CONF_OFF_CODE) + + code_on_receive = config.get(CONF_ON_CODE_RECEIVE, []) + code_off_receive = config.get(CONF_OFF_CODE_RECEIVE, []) + + self._code_on_receive = [] + self._code_off_receive = [] + + for code_list, conf in ( + (self._code_on_receive, code_on_receive), + (self._code_off_receive, code_off_receive), + ): + for code in conf: + echo = code.pop(CONF_ECHO, True) + code_list.append(_ReceiveHandle(code, echo)) + + if any(self._code_on_receive) or any(self._code_off_receive): + hass.bus.listen(EVENT, self._handle_code) + + self._brightness = 255 + + 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._is_on = state.state == STATE_ON + self._brightness = state.attributes.get("brightness") + + @property + def name(self): + """Get the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed, state set when correct code is received.""" + return False + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True + + @property + def is_on(self): + """Return true if switch is on.""" + return self._is_on + + def _handle_code(self, call): + """Check if received code by the pilight-daemon. + + If the code matches the receive on/off codes of this switch the switch + state is changed accordingly. + """ + # - True if off_code/on_code is contained in received code dict, not + # all items have to match. + # - Call turn on/off only once, even if more than one code is received + if any(self._code_on_receive): + for on_code in self._code_on_receive: + if on_code.match(call.data): + on_code.run(switch=self, turn_on=True) + break + + if any(self._code_off_receive): + for off_code in self._code_off_receive: + if off_code.match(call.data): + off_code.run(switch=self, turn_on=False) + break + + def set_state(self, turn_on, send_code=True, dimlevel=None): + """Set the state of the switch. + + This sets the state of the switch. If send_code is set to True, then + it will call the pilight.send service to actually send the codes + to the pilight daemon. + """ + if send_code: + if turn_on: + code = self._code_on + if dimlevel is not None: + code.update({"dimlevel": dimlevel}) + + self._hass.services.call(DOMAIN, SERVICE_NAME, code, blocking=True) + else: + self._hass.services.call( + DOMAIN, SERVICE_NAME, self._code_off, blocking=True + ) + + self._is_on = turn_on + self.schedule_update_ha_state() + + def turn_on(self, **kwargs): + """Turn the switch on by calling pilight.send service with on code.""" + self.set_state(turn_on=True) + + def turn_off(self, **kwargs): + """Turn the switch on by calling pilight.send service with off code.""" + self.set_state(turn_on=False) + + +class _ReceiveHandle: + def __init__(self, config, echo): + """Initialize the handle.""" + self.config_items = config.items() + self.echo = echo + + def match(self, code): + """Test if the received code matches the configured values. + + The received values have to be a subset of the configured options. + """ + return self.config_items <= code.items() + + def run(self, switch, turn_on): + """Change the state of the switch.""" + switch.set_state(turn_on=turn_on, send_code=self.echo) diff --git a/homeassistant/components/pilight/const.py b/homeassistant/components/pilight/const.py new file mode 100644 index 00000000000..3aa53021d31 --- /dev/null +++ b/homeassistant/components/pilight/const.py @@ -0,0 +1,14 @@ +"""Consts used by pilight.""" + +CONF_DIMLEVEL_MAX = "dimlevel_max" +CONF_DIMLEVEL_MIN = "dimlevel_min" +CONF_ECHO = "echo" +CONF_OFF = "off" +CONF_OFF_CODE = "off_code" +CONF_OFF_CODE_RECEIVE = "off_code_receive" +CONF_ON = "on" +CONF_ON_CODE = "on_code" +CONF_ON_CODE_RECEIVE = "on_code_receive" +CONF_SYSTEMCODE = "systemcode" +CONF_UNIT = "unit" +CONF_UNITCODE = "unitcode" diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py new file mode 100644 index 00000000000..49ce3d9a124 --- /dev/null +++ b/homeassistant/components/pilight/light.py @@ -0,0 +1,67 @@ +"""Support for switching devices via Pilight to on and off.""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) +from homeassistant.const import CONF_LIGHTS +import homeassistant.helpers.config_validation as cv + +from .base_class import SWITCHES_SCHEMA, PilightBaseDevice +from .const import CONF_DIMLEVEL_MAX, CONF_DIMLEVEL_MIN + +_LOGGER = logging.getLogger(__name__) + +LIGHTS_SCHEMA = SWITCHES_SCHEMA.extend( + { + vol.Optional(CONF_DIMLEVEL_MIN, default=0): cv.positive_int, + vol.Optional(CONF_DIMLEVEL_MAX, default=15): cv.positive_int, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_LIGHTS): vol.Schema({cv.string: LIGHTS_SCHEMA})} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Pilight platform.""" + switches = config.get(CONF_LIGHTS) + devices = [] + + for dev_name, dev_config in switches.items(): + devices.append(PilightLight(hass, dev_name, dev_config)) + + add_entities(devices) + + +class PilightLight(PilightBaseDevice, Light): + """Representation of a Pilight switch.""" + + def __init__(self, hass, name, config): + """Initialize a switch.""" + super().__init__(hass, name, config) + self._dimlevel_min = config.get(CONF_DIMLEVEL_MIN) + self._dimlevel_max = config.get(CONF_DIMLEVEL_MAX) + + @property + def brightness(self): + """Return the brightness.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + def turn_on(self, **kwargs): + """Turn the switch on by calling pilight.send service with on code.""" + self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + dimlevel = int(self._brightness / (255 / self._dimlevel_max)) + + self.set_state(turn_on=True, dimlevel=dimlevel) diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index e4daeec83cb..b2b2b08f7ff 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": ["pilight==0.1.1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@trekky12"] } diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index 8be199921dc..0700b14e953 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -3,60 +3,14 @@ import logging import voluptuous as vol -from homeassistant.components import pilight from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ( - CONF_ID, - CONF_NAME, - CONF_PROTOCOL, - CONF_STATE, - CONF_SWITCHES, - STATE_ON, -) +from homeassistant.const import CONF_SWITCHES import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.restore_state import RestoreEntity + +from .base_class import SWITCHES_SCHEMA, PilightBaseDevice _LOGGER = logging.getLogger(__name__) -CONF_OFF_CODE = "off_code" -CONF_OFF_CODE_RECEIVE = "off_code_receive" -CONF_ON_CODE = "on_code" -CONF_ON_CODE_RECEIVE = "on_code_receive" -CONF_SYSTEMCODE = "systemcode" -CONF_UNIT = "unit" -CONF_UNITCODE = "unitcode" -CONF_ECHO = "echo" - -COMMAND_SCHEMA = vol.Schema( - { - vol.Optional(CONF_PROTOCOL): cv.string, - vol.Optional("on"): cv.positive_int, - vol.Optional("off"): cv.positive_int, - vol.Optional(CONF_UNIT): cv.positive_int, - vol.Optional(CONF_UNITCODE): cv.positive_int, - vol.Optional(CONF_ID): vol.Any(cv.positive_int, cv.string), - vol.Optional(CONF_STATE): cv.string, - vol.Optional(CONF_SYSTEMCODE): cv.positive_int, - }, - extra=vol.ALLOW_EXTRA, -) - -RECEIVE_SCHEMA = COMMAND_SCHEMA.extend({vol.Optional(CONF_ECHO): cv.boolean}) - -SWITCHES_SCHEMA = vol.Schema( - { - vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, - vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFF_CODE_RECEIVE, default=[]): vol.All( - cv.ensure_list, [COMMAND_SCHEMA] - ), - vol.Optional(CONF_ON_CODE_RECEIVE, default=[]): vol.All( - cv.ensure_list, [COMMAND_SCHEMA] - ), - } -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCHES_SCHEMA})} ) @@ -67,138 +21,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): switches = config.get(CONF_SWITCHES) devices = [] - for dev_name, properties in switches.items(): - devices.append( - PilightSwitch( - hass, - properties.get(CONF_NAME, dev_name), - properties.get(CONF_ON_CODE), - properties.get(CONF_OFF_CODE), - properties.get(CONF_ON_CODE_RECEIVE), - properties.get(CONF_OFF_CODE_RECEIVE), - ) - ) + for dev_name, dev_config in switches.items(): + devices.append(PilightSwitch(hass, dev_name, dev_config)) add_entities(devices) -class _ReceiveHandle: - def __init__(self, config, echo): - """Initialize the handle.""" - self.config_items = config.items() - self.echo = echo - - def match(self, code): - """Test if the received code matches the configured values. - - The received values have to be a subset of the configured options. - """ - return self.config_items <= code.items() - - def run(self, switch, turn_on): - """Change the state of the switch.""" - switch.set_state(turn_on=turn_on, send_code=self.echo) - - -class PilightSwitch(SwitchDevice, RestoreEntity): +class PilightSwitch(PilightBaseDevice, SwitchDevice): """Representation of a Pilight switch.""" - - def __init__( - self, hass, name, code_on, code_off, code_on_receive, code_off_receive - ): - """Initialize the switch.""" - self._hass = hass - self._name = name - self._state = False - self._code_on = code_on - self._code_off = code_off - - self._code_on_receive = [] - self._code_off_receive = [] - - for code_list, conf in ( - (self._code_on_receive, code_on_receive), - (self._code_off_receive, code_off_receive), - ): - for code in conf: - echo = code.pop(CONF_ECHO, True) - code_list.append(_ReceiveHandle(code, echo)) - - if any(self._code_on_receive) or any(self._code_off_receive): - hass.bus.listen(pilight.EVENT, self._handle_code) - - 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): - """Get the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed, state set when correct code is received.""" - return False - - @property - def assumed_state(self): - """Return True if unable to access real state of the entity.""" - return True - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - def _handle_code(self, call): - """Check if received code by the pilight-daemon. - - If the code matches the receive on/off codes of this switch the switch - state is changed accordingly. - """ - # - True if off_code/on_code is contained in received code dict, not - # all items have to match. - # - Call turn on/off only once, even if more than one code is received - if any(self._code_on_receive): - for on_code in self._code_on_receive: - if on_code.match(call.data): - on_code.run(switch=self, turn_on=True) - break - - if any(self._code_off_receive): - for off_code in self._code_off_receive: - if off_code.match(call.data): - off_code.run(switch=self, turn_on=False) - break - - def set_state(self, turn_on, send_code=True): - """Set the state of the switch. - - This sets the state of the switch. If send_code is set to True, then - it will call the pilight.send service to actually send the codes - to the pilight daemon. - """ - if send_code: - if turn_on: - self._hass.services.call( - pilight.DOMAIN, pilight.SERVICE_NAME, self._code_on, blocking=True - ) - else: - self._hass.services.call( - pilight.DOMAIN, pilight.SERVICE_NAME, self._code_off, blocking=True - ) - - self._state = turn_on - self.schedule_update_ha_state() - - def turn_on(self, **kwargs): - """Turn the switch on by calling pilight.send service with on code.""" - self.set_state(turn_on=True) - - def turn_off(self, **kwargs): - """Turn the switch on by calling pilight.send service with off code.""" - self.set_state(turn_on=False)