522 lines
16 KiB
Python
522 lines
16 KiB
Python
"""
|
|
Each device has a specification - list of classes (XEntity childs). Platform
|
|
will setup entity if it isinstance() of platform entity class.
|
|
|
|
User can override SwitchEntity of any device via YAML (device_class option).
|
|
|
|
XEntity properties:
|
|
- params - required, set of parameters that this entity can read
|
|
- param - optional, entity main parameter (useful for sensors)
|
|
- uid - optional, entity unique_id tail
|
|
|
|
Developer can change global properties of existing classes via spec function.
|
|
"""
|
|
|
|
from homeassistant.components.binary_sensor import BinarySensorEntity
|
|
from homeassistant.components.light import LightEntity
|
|
from homeassistant.components.sensor import SensorEntity
|
|
from homeassistant.components.switch import SwitchEntity
|
|
|
|
from .ewelink import XDevice
|
|
from ..binary_sensor import XBinarySensor, XWiFiDoor, XZigbeeMotion
|
|
from ..climate import XClimateNS, XClimateTH, XThermostat
|
|
from ..core.entity import XEntity
|
|
from ..cover import XCover, XCoverDualR3, XZigbeeCover
|
|
from ..fan import XDiffuserFan, XFan, XToggleFan, XFanDualR3
|
|
from ..light import (
|
|
XDiffuserLight,
|
|
XDimmer,
|
|
XFanLight,
|
|
XLight57,
|
|
XLightB1,
|
|
XLightB02,
|
|
XLightB05B,
|
|
XLightD1,
|
|
XLightGroup,
|
|
XLightL1,
|
|
XLightL3,
|
|
XT5Light,
|
|
)
|
|
from ..number import XPulseWidth
|
|
from ..remote import XRemote
|
|
from ..sensor import (
|
|
XEnergySensor,
|
|
XHumidityTH,
|
|
XOutdoorTempNS,
|
|
XRemoteButton,
|
|
XSensor,
|
|
XTemperatureNS,
|
|
XTemperatureTH,
|
|
XUnknown,
|
|
XWiFiDoorBattery,
|
|
XEnergySensorDualR3,
|
|
XEnergySensorPOWR3,
|
|
XEnergyTotal,
|
|
XT5Action,
|
|
)
|
|
from ..switch import (
|
|
XSwitch,
|
|
XSwitches,
|
|
XSwitchTH,
|
|
XToggle,
|
|
XZigbeeSwitches,
|
|
XSwitchPOWR3,
|
|
XDetach,
|
|
)
|
|
|
|
# supported custom device_class
|
|
DEVICE_CLASS = {
|
|
"binary_sensor": (XEntity, BinarySensorEntity),
|
|
"fan": (XToggleFan,), # using custom class for overriding is_on function
|
|
"dualfan": (XFanDualR3,),
|
|
"light": (XEntity, LightEntity),
|
|
"sensor": (XEntity, SensorEntity),
|
|
"switch": (XEntity, SwitchEntity),
|
|
}
|
|
|
|
|
|
def spec(cls, base: str = None, enabled: bool = None, **kwargs) -> type:
|
|
"""Make duplicate for cls class with changes in kwargs params.
|
|
|
|
If `base` param provided - can change Entity base class for cls. So it can
|
|
be added to different Hass domain.
|
|
"""
|
|
if enabled is not None:
|
|
kwargs["_attr_entity_registry_enabled_default"] = enabled
|
|
if base:
|
|
bases = cls.__mro__[-len(XSwitch.__mro__) :: -1]
|
|
bases = {k: v for b in bases for k, v in b.__dict__.items()}
|
|
return type(cls.__name__, DEVICE_CLASS[base], {**bases, **kwargs})
|
|
return type(cls.__name__, (cls,), kwargs)
|
|
|
|
|
|
Switch1 = spec(XSwitches, channel=0, uid="1")
|
|
Switch2 = spec(XSwitches, channel=1, uid="2")
|
|
Switch3 = spec(XSwitches, channel=2, uid="3")
|
|
Switch4 = spec(XSwitches, channel=3, uid="4")
|
|
|
|
XSensor100 = spec(XSensor, multiply=0.01, round=2)
|
|
|
|
Battery = spec(XSensor, param="battery")
|
|
LED = spec(XToggle, param="sledOnline", uid="led", enabled=False)
|
|
RSSI = spec(XSensor, param="rssi", enabled=False)
|
|
PULSE = spec(XToggle, param="pulse", enabled=False)
|
|
|
|
SPEC_SWITCH = [XSwitch, LED, RSSI, PULSE, XPulseWidth]
|
|
SPEC_1CH = [Switch1, LED, RSSI]
|
|
SPEC_2CH = [Switch1, Switch2, LED, RSSI]
|
|
SPEC_3CH = [Switch1, Switch2, Switch3, LED, RSSI]
|
|
SPEC_4CH = [Switch1, Switch2, Switch3, Switch4, LED, RSSI]
|
|
|
|
Current1 = spec(XSensor100, param="current_00", uid="current_1")
|
|
Current2 = spec(XSensor100, param="current_01", uid="current_2")
|
|
Current3 = spec(XSensor100, param="current_02", uid="current_3")
|
|
Current4 = spec(XSensor100, param="current_03", uid="current_4")
|
|
Voltage1 = spec(XSensor100, param="voltage_00", uid="voltage_1")
|
|
Voltage2 = spec(XSensor100, param="voltage_01", uid="voltage_2")
|
|
Voltage3 = spec(XSensor100, param="voltage_02", uid="voltage_3")
|
|
Voltage4 = spec(XSensor100, param="voltage_03", uid="voltage_4")
|
|
Power1 = spec(XSensor100, param="actPow_00", uid="power_1")
|
|
Power2 = spec(XSensor100, param="actPow_01", uid="power_2")
|
|
Power3 = spec(XSensor100, param="actPow_02", uid="power_3")
|
|
Power4 = spec(XSensor100, param="actPow_03", uid="power_4")
|
|
|
|
EnergyPOW = spec(
|
|
XEnergySensor,
|
|
param="hundredDaysKwhData",
|
|
uid="energy",
|
|
get_params={"hundredDaysKwh": "get"},
|
|
)
|
|
|
|
# https://github.com/CoolKit-Technologies/eWeLink-API/blob/main/en/UIIDProtocol.md
|
|
DEVICES = {
|
|
1: SPEC_SWITCH,
|
|
2: SPEC_2CH,
|
|
3: SPEC_3CH,
|
|
4: SPEC_4CH,
|
|
5: [
|
|
XSwitch,
|
|
LED,
|
|
RSSI,
|
|
spec(XSensor, param="power"),
|
|
EnergyPOW,
|
|
], # Sonoff POW (first)
|
|
6: SPEC_SWITCH,
|
|
7: SPEC_2CH, # Sonoff T1 2CH
|
|
8: SPEC_3CH, # Sonoff T1 3CH
|
|
9: SPEC_4CH,
|
|
11: [XCover, LED, RSSI], # King Art - King Q4 Cover (only cloud)
|
|
14: SPEC_SWITCH, # Sonoff Basic (3rd party)
|
|
15: [
|
|
XSwitchTH,
|
|
XClimateTH,
|
|
XTemperatureTH,
|
|
XHumidityTH,
|
|
LED,
|
|
RSSI,
|
|
], # Sonoff TH16
|
|
18: [
|
|
spec(XSensor, param="temperature"),
|
|
spec(XSensor, param="humidity"),
|
|
spec(XSensor, param="dusty"),
|
|
spec(XSensor, param="light"),
|
|
spec(XSensor, param="noise"),
|
|
],
|
|
22: [XLightB1, RSSI], # Sonoff B1 (only cloud)
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/173
|
|
25: [
|
|
XDiffuserFan,
|
|
XDiffuserLight,
|
|
RSSI,
|
|
spec(XBinarySensor, param="water", uid=""),
|
|
], # Diffuser
|
|
28: [XRemote, LED, RSSI], # Sonoff RF Brigde 433
|
|
29: SPEC_2CH,
|
|
30: SPEC_3CH,
|
|
31: SPEC_4CH,
|
|
32: [
|
|
XSwitch,
|
|
LED,
|
|
RSSI,
|
|
spec(XSensor, param="current"),
|
|
spec(XSensor, param="power"),
|
|
spec(XSensor, param="voltage"),
|
|
EnergyPOW,
|
|
], # Sonoff POWR2
|
|
33: [XLightL1, RSSI], # https://github.com/AlexxIT/SonoffLAN/issues/985
|
|
34: [
|
|
XFan,
|
|
XFanLight,
|
|
LED,
|
|
RSSI,
|
|
], # Sonoff iFan02 and iFan03
|
|
36: [XDimmer, RSSI], # KING-M4 (dimmer, only cloud)
|
|
44: [XLightD1, RSSI], # Sonoff D1
|
|
57: [XLight57, RSSI], # Mosquito Killer Lamp
|
|
59: [XLightL1, RSSI], # Sonoff LED (only cloud)
|
|
66: [RSSI, LED, spec(XBinarySensor, param="zled", enabled=False)], # ZigBee Bridge
|
|
77: SPEC_1CH, # Sonoff Micro
|
|
78: SPEC_1CH, # https://github.com/AlexxIT/SonoffLAN/issues/615
|
|
81: SPEC_1CH,
|
|
82: SPEC_2CH,
|
|
83: SPEC_3CH,
|
|
84: SPEC_4CH,
|
|
102: [XWiFiDoor, XWiFiDoorBattery, RSSI], # Sonoff DW2 Door/Window sensor
|
|
103: [XLightB02, RSSI], # Sonoff B02 CCT bulb
|
|
104: [XLightB05B, RSSI], # Sonoff B05-B RGB+CCT color bulb
|
|
107: SPEC_1CH,
|
|
126: [
|
|
Switch1,
|
|
Switch2,
|
|
RSSI,
|
|
Current1,
|
|
Current2,
|
|
Voltage1,
|
|
Voltage2,
|
|
Power1,
|
|
Power2,
|
|
spec(
|
|
XEnergySensorDualR3,
|
|
param="kwhHistories_00",
|
|
uid="energy_1",
|
|
get_params={"getKwh_00": 2},
|
|
),
|
|
spec(
|
|
XEnergySensorDualR3,
|
|
param="kwhHistories_01",
|
|
uid="energy_2",
|
|
get_params={"getKwh_01": 2},
|
|
),
|
|
], # Sonoff DualR3
|
|
127: [XThermostat], # https://github.com/AlexxIT/SonoffLAN/issues/358
|
|
128: [LED], # SPM-Main
|
|
130: [
|
|
Switch1,
|
|
Switch2,
|
|
Switch3,
|
|
Switch4,
|
|
Current1,
|
|
Current2,
|
|
Current3,
|
|
Current4,
|
|
Voltage1,
|
|
Voltage2,
|
|
Voltage3,
|
|
Voltage4,
|
|
Power1,
|
|
Power2,
|
|
Power3,
|
|
Power4,
|
|
spec(
|
|
XEnergySensorDualR3,
|
|
param="kwhHistories_00",
|
|
uid="energy_1",
|
|
get_params={"getKwh_00": 2},
|
|
),
|
|
spec(
|
|
XEnergySensorDualR3,
|
|
param="kwhHistories_01",
|
|
uid="energy_2",
|
|
get_params={"getKwh_01": 2},
|
|
),
|
|
spec(
|
|
XEnergySensorDualR3,
|
|
param="kwhHistories_01",
|
|
uid="energy_3",
|
|
get_params={"getKwh_02": 2},
|
|
),
|
|
spec(
|
|
XEnergySensorDualR3,
|
|
param="kwhHistories_01",
|
|
uid="energy_4",
|
|
get_params={"getKwh_03": 2},
|
|
),
|
|
], # SPM-4Relay, https://github.com/AlexxIT/SonoffLAN/issues/658
|
|
133: [
|
|
# Humidity. ALWAYS 50... NSPanel DOESN'T HAVE HUMIDITY SENSOR
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/751
|
|
Switch1,
|
|
Switch2,
|
|
XClimateNS,
|
|
XTemperatureNS,
|
|
XOutdoorTempNS,
|
|
], # Sonoff NS Panel
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/1026
|
|
135: [XLightB02, RSSI], # Sonoff B02-BL
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/766
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/890
|
|
# https://github.com/AlexxIT/SonoffLAN/pull/892
|
|
# https://github.com/AlexxIT/SonoffLAN/pull/1035
|
|
136: [spec(XLightB05B, min_ct=0, max_ct=100), RSSI], # Sonoff B05-BL
|
|
137: [XLightL1, RSSI],
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/623#issuecomment-1365841454
|
|
138: [
|
|
Switch1,
|
|
LED,
|
|
RSSI,
|
|
XDetach,
|
|
spec(XRemoteButton, param="action"),
|
|
], # MINIR3, MINIR4
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/808
|
|
154: [XWiFiDoor, Battery, RSSI], # DW2-Wi-Fi-L
|
|
162: SPEC_3CH, # https://github.com/AlexxIT/SonoffLAN/issues/659
|
|
165: [Switch1, Switch2, RSSI], # DualR3 Lite, without power consumption
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/857
|
|
168: [RSSI], # new ZBBridge-P
|
|
173: [XLightL3, RSSI], # Sonoff L3-5M-P
|
|
174: [XRemoteButton], # Sonoff R5 (6-key remote)
|
|
177: [XRemoteButton], # Sonoff S-Mate
|
|
181: [
|
|
XSwitchTH,
|
|
XTemperatureTH,
|
|
XHumidityTH,
|
|
LED,
|
|
RSSI,
|
|
], # Sonoff THR320D or THR316D
|
|
182: [
|
|
Switch1,
|
|
LED,
|
|
RSSI,
|
|
spec(XSensor, param="current"),
|
|
spec(XSensor, param="power"),
|
|
spec(XSensor, param="voltage"),
|
|
EnergyPOW,
|
|
], # Sonoff S40
|
|
190: [
|
|
XSwitchPOWR3,
|
|
LED,
|
|
RSSI,
|
|
spec(XSensor100, param="current"),
|
|
spec(XSensor100, param="power"),
|
|
spec(XSensor100, param="voltage"),
|
|
spec(XEnergyTotal, param="dayKwh", uid="energy_day", multiply=0.01, round=2),
|
|
spec(
|
|
XEnergyTotal, param="monthKwh", uid="energy_month", multiply=0.01, round=2
|
|
),
|
|
spec(
|
|
XEnergySensorPOWR3,
|
|
param="hoursKwhData",
|
|
uid="energy",
|
|
get_params={"getHoursKwh": {"start": 0, "end": 24 * 30 - 1}},
|
|
),
|
|
], # Sonoff POWR3
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/984
|
|
195: [XTemperatureTH], # NSPanel Pro
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/1183
|
|
209: [Switch1, XT5Light, XT5Action], # T5-1C-86
|
|
210: [Switch1, Switch2, XT5Light, XT5Action], # T5-2C-86
|
|
211: [Switch1, Switch2, Switch3, XT5Light, XT5Action], # T5-3C-86
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/1251
|
|
212: [Switch1, Switch2, Switch3, Switch4, XT5Light, XT5Action], # T5-4C-86
|
|
1000: [XRemoteButton, Battery], # zigbee_ON_OFF_SWITCH_1000
|
|
1256: [spec(XSwitch, base="light")], # ZCL_HA_DEVICEID_ON_OFF_LIGHT
|
|
1257: [XLightD1], # ZigbeeWhiteLight
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/972
|
|
1514: [XZigbeeCover, spec(XSensor, param="battery", multiply=2)],
|
|
1770: [
|
|
spec(XSensor100, param="temperature"),
|
|
spec(XSensor100, param="humidity"),
|
|
Battery,
|
|
], # ZCL_HA_DEVICEID_TEMPERATURE_SENSOR
|
|
1771: [
|
|
spec(XSensor100, param="temperature"),
|
|
spec(XSensor100, param="humidity"),
|
|
Battery,
|
|
], # https://github.com/AlexxIT/SonoffLAN/issues/1150
|
|
2026: [XZigbeeMotion, Battery], # ZIGBEE_MOBILE_SENSOR
|
|
# ZIGBEE_DOOR_AND_WINDOW_SENSOR
|
|
3026: [
|
|
# backward compatibility for unique_id
|
|
spec(XBinarySensor, param="lock", uid="", default_class="door"),
|
|
Battery,
|
|
],
|
|
4026: [
|
|
spec(XBinarySensor, param="water", uid="", default_class="moisture"),
|
|
Battery,
|
|
], # https://github.com/AlexxIT/SonoffLAN/issues/852
|
|
4256: [
|
|
spec(XZigbeeSwitches, channel=0, uid="1"),
|
|
spec(XZigbeeSwitches, channel=1, uid="2"),
|
|
spec(XZigbeeSwitches, channel=2, uid="3"),
|
|
spec(XZigbeeSwitches, channel=3, uid="4"),
|
|
],
|
|
7000: [
|
|
XRemoteButton,
|
|
Battery,
|
|
],
|
|
7014: [
|
|
spec(XSensor100, param="temperature"),
|
|
spec(XSensor100, param="humidity"),
|
|
Battery,
|
|
], # https://github.com/AlexxIT/SonoffLAN/issues/1166
|
|
}
|
|
|
|
|
|
def get_spec(device: dict) -> list:
|
|
uiid = device["extra"]["uiid"]
|
|
|
|
if uiid in DEVICES:
|
|
classes = DEVICES[uiid]
|
|
elif "switch" in device["params"]:
|
|
classes = SPEC_SWITCH
|
|
elif "switches" in device["params"]:
|
|
classes = SPEC_4CH
|
|
else:
|
|
classes = [XUnknown]
|
|
|
|
# DualR3 in cover mode
|
|
if uiid in [126, 165] and device["params"].get("workMode") == 2:
|
|
classes = [cls for cls in classes if XSwitches not in cls.__bases__]
|
|
classes.insert(0, XCoverDualR3)
|
|
|
|
# NSPanel Climate disable without switch configuration
|
|
if uiid in [133] and not device["params"].get("HMI_ATCDevice"):
|
|
classes = [cls for cls in classes if XClimateNS not in cls.__bases__]
|
|
|
|
if "device_class" in device:
|
|
classes = get_custom_spec(classes, device["device_class"])
|
|
|
|
return classes
|
|
|
|
|
|
def get_custom_spec(classes: list, device_class):
|
|
"""Supported device_class formats:
|
|
1. Single channel:
|
|
device_class: light
|
|
2. Multiple channels:
|
|
device_class: [light, fan, switch]
|
|
3. Light with brightness control
|
|
device_class:
|
|
- switch # entity 1 (channel 1)
|
|
- light: [2, 3] # entity 2 (channels 2 and 3)
|
|
- fan: 4 # entity 3 (channel 4)
|
|
"""
|
|
# 1. single channel
|
|
if isinstance(device_class, str):
|
|
if device_class in DEVICE_CLASS:
|
|
classes = [spec(classes[0], base=device_class)] + classes[1:]
|
|
|
|
elif isinstance(device_class, list):
|
|
# remove all default multichannel classes from spec
|
|
base = classes[0].__base__
|
|
classes = [cls for cls in classes if base not in cls.__bases__]
|
|
|
|
for i, sub_class in enumerate(device_class):
|
|
# 2. simple multichannel
|
|
if isinstance(sub_class, str):
|
|
classes.append(spec(base, channel=i, uid=str(i + 1), base=sub_class))
|
|
|
|
elif isinstance(sub_class, dict):
|
|
sub_class, i = next(iter(sub_class.items()))
|
|
|
|
# 3. light with brightness
|
|
if isinstance(i, list) and sub_class == "light":
|
|
chs = [x - 1 for x in i]
|
|
uid = "".join(str(x) for x in i)
|
|
classes.append(spec(XLightGroup, channels=chs, uid=uid))
|
|
|
|
# 4. multichannel
|
|
elif isinstance(i, int):
|
|
classes.append(
|
|
spec(base, channel=(i - 1), uid=str(i), base=sub_class)
|
|
)
|
|
|
|
return classes
|
|
|
|
|
|
def get_spec_wrapper(func, sensors: list):
|
|
def wrapped(device: dict) -> list:
|
|
classes = func(device)
|
|
for uid in sensors:
|
|
if (uid in device["params"] or uid == "host") and all(
|
|
cls.param != uid and cls.uid != uid for cls in classes
|
|
):
|
|
classes.append(spec(XSensor, param=uid))
|
|
return classes
|
|
|
|
return wrapped
|
|
|
|
|
|
def set_default_class(device_class: str):
|
|
XSwitch.__bases__ = XSwitches.__bases__ = (
|
|
XEntity,
|
|
LightEntity if device_class == "light" else SwitchEntity,
|
|
)
|
|
|
|
|
|
# Cloud: NSPanel
|
|
DIY = {
|
|
# DIY type, UIID, Brand, Model/Name
|
|
"plug": [1, None, "Single Channel DIY"], # POWR316
|
|
"strip": [4, None, "Multi Channel DIY"], # 4CHPROR3
|
|
"diy_plug": [1, "SONOFF", "MINI DIY"],
|
|
"enhanced_plug": [5, "SONOFF", "POW DIY"], # POWR2
|
|
"th_plug": [15, "SONOFF", "TH DIY"], # TH16R2
|
|
"rf": [28, "SONOFF", "RFBridge DIY"],
|
|
"fan_light": [34, "SONOFF", "iFan DIY"],
|
|
"light": [44, "SONOFF", "D1 DIY"], # don't know if light exist
|
|
"diylight": [44, "SONOFF", "D1 DIY"],
|
|
"switch_radar": [77, "SONOFF", "Micro DIY"], # Micro
|
|
"multifun_switch": [126, "SONOFF", "DualR3 DIY"],
|
|
}
|
|
|
|
|
|
def setup_diy(device: dict) -> XDevice:
|
|
ltype = device["localtype"]
|
|
try:
|
|
uiid, brand, model = DIY[ltype]
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/1136
|
|
# https://github.com/AlexxIT/SonoffLAN/issues/1156
|
|
if ltype == "diy_plug" and "switches" in device["params"]:
|
|
uiid = 77
|
|
model = "MINI R3 DIY"
|
|
device["name"] = model
|
|
device["brandName"] = brand
|
|
device["extra"] = {"uiid": uiid}
|
|
device["productModel"] = model
|
|
except Exception:
|
|
device["name"] = "Unknown DIY"
|
|
device["extra"] = {"uiid": 0}
|
|
device["productModel"] = ltype
|
|
# device["online"] = False
|
|
return device
|