File: /volume1/docker/homeassistant2025/custom_components/sonoff/remote.py
import asyncio
import logging
from typing import Union
from homeassistant.components.remote import (
ATTR_DELAY_SECS,
DEFAULT_DELAY_SECS,
RemoteEntity,
)
from homeassistant.const import ATTR_COMMAND
from homeassistant.helpers.entity import Entity
from .binary_sensor import XRemoteSensor, XRemoteSensorOff
from .button import XRemoteButton
from .core.const import DOMAIN
from .core.entity import XEntity
from .core.ewelink import SIGNAL_ADD_ENTITIES, XRegistry
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0 # fix entity_platform parallel_updates Semaphore
async def async_setup_entry(hass, config_entry, add_entities):
ewelink: XRegistry = hass.data[DOMAIN][config_entry.entry_id]
ewelink.dispatcher_connect(
SIGNAL_ADD_ENTITIES,
lambda x: add_entities([e for e in x if isinstance(e, RemoteEntity)]),
)
def rfbridge_childs(remotes: list, config: dict = None):
childs = {}
# For dual RF sensors: {payload_on channel: payload_off name}
duals = {}
for remote in remotes:
for button in remote["buttonName"]:
channel = next(iter(button))
# remote_type 6 (alarm) has one button without name
if remote["remote_type"] != "6":
child = {"name": button[channel], "device_class": "button"}
else:
child = {"name": remote["name"]}
# everride child params from YAML
if config and child["name"] in config:
child.update(config[child["name"]])
if "payload_off" in child:
duals[channel] = child["payload_off"]
# child with timeout or payload_off can't be a button
if child.get("device_class") == "button" and (
"payload_off" in child or "timeout" in child
):
child.pop("device_class")
child["channel"] = channel
childs[channel] = child
for ch, name in duals.items():
try:
ch_off = next(k for k, v in childs.items() if v["name"] == name)
except StopIteration:
_LOGGER.warning("Can't find payload_off: " + name)
continue
# move off channel to end of the dict
childs[ch_off] = childs.pop(ch_off)
childs[ch_off]["channel_on"] = ch
return childs
# noinspection PyAbstractClass
class XRemote(XEntity, RemoteEntity):
_attr_is_on = True
childs: dict[str, Union[XRemoteButton, XRemoteSensor, XRemoteSensorOff]] = None
def __init__(self, ewelink: XRegistry, device: dict):
try:
# only learned channels
channels = [str(c["rfChl"]) for c in device["params"]["rfList"]]
config = ewelink.config and ewelink.config.get("rfbridge")
childs = rfbridge_childs(device["tags"]["zyx_info"], config)
for ch, child in list(childs.items()):
if ch not in channels:
childs.pop(ch)
continue
if "channel_on" in child:
sensor = childs[child["channel_on"]]
childs[ch] = XRemoteSensorOff(child, sensor)
elif child.get("device_class") == "button":
childs[ch] = XRemoteButton(ewelink, device, child)
else:
childs[ch] = XRemoteSensor(ewelink, device, child)
ewelink.dispatcher_send(SIGNAL_ADD_ENTITIES, childs.values())
self.childs = childs
except Exception as e:
_LOGGER.error(f"{self.unique_id} | can't setup RFBridge", exc_info=e)
# init bridge after childs for update available
XEntity.__init__(self, ewelink, device)
self.params = {"cmd", "arming"}
self.ts = None
def set_state(self, params: dict):
# skip full cloud state update
# https://github.com/AlexxIT/SonoffLAN/issues/1101
if not self.is_on or "init" in params or not self.hass:
return
for param, ts in params.items():
if not param.startswith("rfTrig"):
continue
# skip first msg from LAN because it sent old trigger event with
# local discovery and only LAN sends arming param
if self.ts is None and params.get("arming"):
self.ts = ts
return
# skip same cmd from local and cloud
if ts == self.ts:
return
self.ts = ts
child = self.childs.get(param[6:])
if not child:
return
child.internal_update(ts)
self._attr_extra_state_attributes = data = {
"command": int(child.channel),
"name": child.name,
"entity_id": self.entity_id,
"ts": ts,
}
self.hass.bus.async_fire("sonoff.remote", data)
def internal_available(self) -> bool:
available = XEntity.internal_available(self)
if self.childs and self.available != available:
# update available for all entity childs
for child in self.childs.values():
if not isinstance(child, Entity):
continue
child._attr_available = available
if child.hass:
child._async_write_ha_state()
return available
async def async_send_command(self, command, **kwargs):
delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
for i, channel in enumerate(command):
if i:
await asyncio.sleep(delay)
# transform button name to channel number
if not channel.isdigit():
channel = next(k for k, v in self.childs.items() if v.name == channel)
# cmd param for local and for cloud mode
await self.ewelink.send(
self.device,
{"cmd": "transmit", "rfChl": int(channel)},
cmd_lan="transmit",
)
async def async_learn_command(self, **kwargs):
command = kwargs[ATTR_COMMAND]
# cmd param for local and for cloud mode
await self.ewelink.send(
self.device, {"cmd": "capture", "rfChl": int(command[0])}, cmd_lan="capture"
)
async def async_turn_on(self, **kwargs) -> None:
self._attr_is_on = True
self._async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None:
self._attr_is_on = False
self._async_write_ha_state()