diff --git a/custom_components/smartir/climate.py b/custom_components/smartir/climate.py index 01aa7032..fd7ccab0 100644 --- a/custom_components/smartir/climate.py +++ b/custom_components/smartir/climate.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_conversion import TemperatureConverter from . import DeviceData -from .controller import get_controller +from .controller import get_controller, get_controller_schema _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,6 @@ CONF_UNIQUE_ID = "unique_id" CONF_DEVICE_CODE = "device_code" CONF_CONTROLLER_DATA = "controller_data" -CONF_CONTROLLER_PARAMS = "controller_params" CONF_DELAY = "delay" CONF_TEMPERATURE_SENSOR = "temperature_sensor" CONF_HUMIDITY_SENSOR = "humidity_sensor" @@ -57,8 +56,7 @@ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_DEVICE_CODE): cv.positive_int, - vol.Required(CONF_CONTROLLER_DATA): cv.string, - vol.Optional(CONF_CONTROLLER_PARAMS, default={}): dict, + vol.Required(CONF_CONTROLLER_DATA): get_controller_schema(vol, cv), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_float, vol.Optional(CONF_TEMPERATURE_SENSOR): cv.entity_id, vol.Optional(CONF_HUMIDITY_SENSOR): cv.entity_id, @@ -107,7 +105,6 @@ def __init__(self, hass, config, device_data): self._name = config.get(CONF_NAME) self._device_code = config.get(CONF_DEVICE_CODE) self._controller_data = config.get(CONF_CONTROLLER_DATA) - self._controller_params = config.get(CONF_CONTROLLER_PARAMS) self._delay = config.get(CONF_DELAY) self._temperature_sensor = config.get(CONF_TEMPERATURE_SENSOR) self._humidity_sensor = config.get(CONF_HUMIDITY_SENSOR) @@ -224,13 +221,11 @@ def __init__(self, hass, config, device_data): self._temp_lock = asyncio.Lock() # Init the IR/RF controller - self._controller_params["delay"] = self._delay self._controller = get_controller( self.hass, self._supported_controller, self._commands_encoding, self._controller_data, - self._controller_params, ) async def async_added_to_hass(self): @@ -604,7 +599,7 @@ async def _send_command( ): # prevent to resend 'off' command if same as 'on' and device is already off _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is alredy in requested '%s' state skipping sending '%s' command", + "As 'on' and 'off' commands are identical and device is already in requested '%s' state skipping sending '%s' command", self._state, "off", ) @@ -631,7 +626,7 @@ async def _send_command( ): # prevent to resend 'on' command if same as 'off' and device is already on _LOGGER.debug( - "As 'on' and 'off' commands are identical and device is alredy in requested '%s' state skipping sending '%s' command", + "As 'on' and 'off' commands are identical and device is already in requested '%s' state skipping sending '%s' command", self._state, "on", ) diff --git a/custom_components/smartir/controller.py b/custom_components/smartir/controller.py index cd95467b..5f6fa040 100644 --- a/custom_components/smartir/controller.py +++ b/custom_components/smartir/controller.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod from base64 import b64encode +import ipaddress import binascii import requests import struct @@ -22,12 +23,13 @@ ESPHOME_COMMANDS_ENCODING, ZHA_COMMANDS_ENCODING, UFOR11_COMMANDS_ENCODING, + CONTROLLER_CONF, ) from homeassistant.const import ATTR_ENTITY_ID -def get_controller(hass, controller, encoding, controller_data, controller_params): +def get_controller(hass, controller, encoding, controller_data): """Return a controller compatible with the specification provided.""" controllers = { BROADLINK_CONTROLLER: BroadlinkController, @@ -38,23 +40,84 @@ def get_controller(hass, controller, encoding, controller_data, controller_param ZHA_CONTROLLER: ZHAController, UFOR11_CONTROLLER: UFOR11Controller, } - try: - return controllers[controller]( - hass, controller, encoding, controller_data, controller_params - ) - except KeyError: + + #check controller compatibility + if controller not in controllers: + raise Exception("The controller is not supported.") + + if controller_data["controller_type"] != controller: raise Exception("The controller is not supported.") + return controllers[controller]( + hass, controller, encoding, controller_data + ) + + +def get_controller_schema(vol, cv): + """Return a controller schema.""" + schema = vol.Any( + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(BROADLINK_CONTROLLER), + vol.Required(CONTROLLER_CONF["REMOTE_ENTITY"]): cv.entity_id, + vol.Optional(CONTROLLER_CONF["NUM_REPEATS"]): cv.positive_int, + vol.Optional(CONTROLLER_CONF["DELAY_SECS"]): cv.positive_float, + } + ), + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(XIAOMI_CONTROLLER), + vol.Required(CONTROLLER_CONF["REMOTE_ENTITY"]): cv.entity_id, + } + ), + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(MQTT_CONTROLLER), + vol.Required(CONTROLLER_CONF["MQTT_TOPIC"]): cv.string, + } + ), + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(UFOR11_CONTROLLER), + vol.Required(CONTROLLER_CONF["MQTT_TOPIC"]): cv.string, + } + ), + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(LOOKIN_CONTROLLER), + vol.Required(CONTROLLER_CONF["REMOTE_HOST"]): vol.All(ipaddress.ip_address, cv.string), + } + ), + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(ESPHOME_CONTROLLER), + vol.Required(CONTROLLER_CONF["ESPHOME_SERVICE"]): cv.string, + } + ), + vol.Schema( + { + vol.Required(CONTROLLER_CONF["CONTROLLER_TYPE"]): vol.Equal(ZHA_CONTROLLER), + vol.Required(CONTROLLER_CONF["ZHA_IEEE"]): cv.string, + vol.Required(CONTROLLER_CONF["ZHA_ENDPOINT_ID"]): cv.positive_int, + vol.Required(CONTROLLER_CONF["ZHA_CLUSTER_ID"]): cv.positive_int, + vol.Required(CONTROLLER_CONF["ZHA_CLUSTER_TYPE"]): cv.string, + vol.Required(CONTROLLER_CONF["ZHA_COMMAND"]): cv.positive_int, + vol.Required(CONTROLLER_CONF["ZHA_COMMAND_TYPE"]): cv.string, + } + ), + ) + + return schema + class AbstractController(ABC): """Representation of a controller.""" - def __init__(self, hass, controller, encoding, controller_data, controller_params): + def __init__(self, hass, controller, encoding, controller_data): self.hass = hass self._controller = controller self._encoding = encoding self._controller_data = controller_data - self._controller_params = controller_params @abstractmethod def check_encoding(self, encoding): @@ -107,15 +170,13 @@ async def send(self, command): commands.append("b64:" + _command) service_data = { - ATTR_ENTITY_ID: self._controller_data, + ATTR_ENTITY_ID: self._controller_data[CONTROLLER_CONF["REMOTE_ENTITY"]], "command": commands, } - if "delay_secs" in self._controller_params: - service_data["delay_secs"] = self._controller_params["delay_secs"] - else: - service_data["delay_secs"] = self._controller_params["delay"] - if "num_repeats" in self._controller_params: - service_data["num_repeats"] = self._controller_params["num_repeats"] + if CONTROLLER_CONF["DELAY_SECS"] in self._controller_data: + service_data["delay_secs"] = self._controller_data[CONTROLLER_CONF["DELAY_SECS"]] + if CONTROLLER_CONF["NUM_REPEATS"] in self._controller_data: + service_data["num_repeats"] = self._controller_data[CONTROLLER_CONF["NUM_REPEATS"]] await self.hass.services.async_call("remote", "send_command", service_data) @@ -133,7 +194,7 @@ def check_encoding(self, encoding): async def send(self, command): """Send a command.""" service_data = { - ATTR_ENTITY_ID: self._controller_data, + ATTR_ENTITY_ID: self._controller_data[CONTROLLER_CONF["REMOTE_ENTITY"]], "command": self._encoding.lower() + ":" + command, } @@ -150,7 +211,10 @@ def check_encoding(self, encoding): async def send(self, command): """Send a command.""" - service_data = {"topic": self._controller_data, "payload": command} + service_data = { + "topic": self._controller_data[CONTROLLER_CONF["MQTT_TOPIC"]], + "payload": command, + } await self.hass.services.async_call("mqtt", "publish", service_data) @@ -168,7 +232,7 @@ def check_encoding(self, encoding): async def send(self, command): """Send a command.""" encoding = self._encoding.lower().replace("pronto", "prontohex") - url = f"http://{self._controller_data}/commands/ir/" f"{encoding}/{command}" + url = f"http://{self._controller_data[CONTROLLER_CONF["REMOTE_HOST"]]}/commands/ir/" f"{encoding}/{command}" await self.hass.async_add_executor_job(requests.get, url) @@ -187,7 +251,7 @@ async def send(self, command): service_data = {"command": json.loads(command)} await self.hass.services.async_call( - "esphome", self._controller_data, service_data + "esphome", self._controller_data[CONTROLLER_CONF["ESPHOME_SERVICE"]], service_data ) @@ -203,16 +267,14 @@ def check_encoding(self, encoding): async def send(self, command): """Send a command.""" - service_data = json.loads(self._controller_data) - if not isinstance(service_data, dict): - raise Exception("Wrong json config for ZHA controller") - for key in ["ieee", "endpoint_id", "cluster_id", "cluster_type", "command"]: - if not service_data.get(key): - raise Exception( - "Missing '%s' parameter in config for ZHA controller", key - ) - service_data["params"] = { - "code": command, + service_data = { + "ieee": self._controller_data[CONTROLLER_CONF["ZHA_IEEE"]], + "endpoint_id": self._controller_data[CONTROLLER_CONF["ZHA_ENDPOINT_ID"]], + "cluster_id": self._controller_data[CONTROLLER_CONF["ZHA_CLUSTER_ID"]], + "cluster_type": self._controller_data[CONTROLLER_CONF["ZHA_CLUSTER_TYPE"]], + "command": self._controller_data[CONTROLLER_CONF["ZHA_COMMAND"]], + "command_type": self._controller_data[CONTROLLER_CONF["ZHA_COMMAND_TYPE"]], + "params": { "code": command }, } await self.hass.services.async_call( "zha", "issue_zigbee_cluster_command", service_data @@ -230,7 +292,7 @@ def check_encoding(self, encoding): async def send(self, command): """Send a command.""" service_data = { - "topic": self._controller_data, + "topic": self._controller_data[CONTROLLER_CONF["MQTT_TOPIC"]], "payload": json.dumps({"ir_code_to_send": command}), } diff --git a/custom_components/smartir/controller_const.py b/custom_components/smartir/controller_const.py index abc59c6c..f074c443 100644 --- a/custom_components/smartir/controller_const.py +++ b/custom_components/smartir/controller_const.py @@ -28,3 +28,19 @@ ZHA_CONTROLLER: ZHA_COMMANDS_ENCODING, UFOR11_CONTROLLER: UFOR11_COMMANDS_ENCODING, } + +CONTROLLER_CONF = { + "CONTROLLER_TYPE": "controller_type", + "REMOTE_ENTITY": "remote_entity", + "NUM_REPEATS": "num_repeats", + "DELAY_SECS": "delay_secs", + "MQTT_TOPIC": "delay_secs", + "REMOTE_HOST": "remote_host", + "ESPHOME_SERVICE": "esphome_service", + "ZHA_IEEE": "zha_ieee", + "ZHA_ENDPOINT_ID": "zha_endpoint_id", + "ZHA_CLUSTER_ID": "zha_cluster_id", + "ZHA_CLUSTER_TYPE": "zha_cluster_type", + "ZHA_COMMAND": "zha_command", + "ZHA_COMMAND_TYPE": "zha_command_type", +} diff --git a/custom_components/smartir/fan.py b/custom_components/smartir/fan.py index 1e69de3c..a7e8cd2a 100644 --- a/custom_components/smartir/fan.py +++ b/custom_components/smartir/fan.py @@ -21,7 +21,7 @@ percentage_to_ordered_list_item, ) from . import DeviceData -from .controller import get_controller +from .controller import get_controller, get_controller_schema _LOGGER = logging.getLogger(__name__) @@ -32,7 +32,6 @@ CONF_UNIQUE_ID = "unique_id" CONF_DEVICE_CODE = "device_code" CONF_CONTROLLER_DATA = "controller_data" -CONF_CONTROLLER_PARAMS = "controller_params" CONF_DELAY = "delay" CONF_POWER_SENSOR = "power_sensor" CONF_POWER_SENSOR_DELAY = "power_sensor_delay" @@ -45,8 +44,7 @@ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_DEVICE_CODE): cv.positive_int, - vol.Required(CONF_CONTROLLER_DATA): cv.string, - vol.Optional(CONF_CONTROLLER_PARAMS, default={}): dict, + vol.Required(CONF_CONTROLLER_DATA): get_controller_schema(vol, cv), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_float, vol.Optional(CONF_POWER_SENSOR): cv.entity_id, vol.Optional( @@ -85,7 +83,6 @@ def __init__(self, hass, config, device_data): self._name = config.get(CONF_NAME) self._device_code = config.get(CONF_DEVICE_CODE) self._controller_data = config.get(CONF_CONTROLLER_DATA) - self._controller_params = config.get(CONF_CONTROLLER_PARAMS) self._delay = config.get(CONF_DELAY) self._power_sensor = config.get(CONF_POWER_SENSOR) self._power_sensor_delay = config.get(CONF_POWER_SENSOR_DELAY) @@ -128,13 +125,11 @@ def __init__(self, hass, config, device_data): self._temp_lock = asyncio.Lock() # Init the IR/RF controller - self._controller_params["delay"] = self._delay self._controller = get_controller( self.hass, self._supported_controller, self._commands_encoding, self._controller_data, - self._controller_params, ) async def async_added_to_hass(self): diff --git a/custom_components/smartir/media_player.py b/custom_components/smartir/media_player.py index 7097c0f4..bb535be9 100644 --- a/custom_components/smartir/media_player.py +++ b/custom_components/smartir/media_player.py @@ -15,7 +15,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from . import DeviceData -from .controller import get_controller +from .controller import get_controller, get_controller_schema _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,6 @@ CONF_UNIQUE_ID = "unique_id" CONF_DEVICE_CODE = "device_code" CONF_CONTROLLER_DATA = "controller_data" -CONF_CONTROLLER_PARAMS = "controller_params" CONF_DELAY = "delay" CONF_POWER_SENSOR = "power_sensor" CONF_POWER_SENSOR_DELAY = "power_sensor_delay" @@ -40,8 +39,7 @@ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_DEVICE_CODE): cv.positive_int, - vol.Required(CONF_CONTROLLER_DATA): cv.string, - vol.Optional(CONF_CONTROLLER_PARAMS, default={}): dict, + vol.Required(CONF_CONTROLLER_DATA): get_controller_schema(vol, cv), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_float, vol.Optional(CONF_POWER_SENSOR): cv.entity_id, vol.Optional( @@ -82,7 +80,6 @@ def __init__(self, hass, config, device_data): self._name = config.get(CONF_NAME) self._device_code = config.get(CONF_DEVICE_CODE) self._controller_data = config.get(CONF_CONTROLLER_DATA) - self._controller_params = config.get(CONF_CONTROLLER_PARAMS) self._delay = config.get(CONF_DELAY) self._power_sensor = config.get(CONF_POWER_SENSOR) self._power_sensor_delay = config.get(CONF_POWER_SENSOR_DELAY) @@ -164,13 +161,11 @@ def __init__(self, hass, config, device_data): self._temp_lock = asyncio.Lock() # Init the IR/RF controller - self._controller_params["delay"] = self._delay self._controller = get_controller( self.hass, self._supported_controller, self._commands_encoding, self._controller_data, - self._controller_params, ) async def async_added_to_hass(self):