Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add unified configuration validation schema for all controller types #89

Merged
merged 1 commit into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions custom_components/smartir/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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"
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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",
)
Expand All @@ -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",
)
Expand Down
122 changes: 92 additions & 30 deletions custom_components/smartir/controller.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from base64 import b64encode
import ipaddress
import binascii
import requests
import struct
Expand All @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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,
}

Expand All @@ -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)

Expand All @@ -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)


Expand All @@ -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
)


Expand All @@ -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
Expand All @@ -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}),
}

Expand Down
16 changes: 16 additions & 0 deletions custom_components/smartir/controller_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
9 changes: 2 additions & 7 deletions custom_components/smartir/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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"
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
9 changes: 2 additions & 7 deletions custom_components/smartir/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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"
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
Loading