Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
70 changes: 70 additions & 0 deletions tests/test_lixee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Tests for LiXee ZLinky_TIC quirks."""

from zha.quirks import DEVICE_REGISTRY
from zigpy.zcl.clusters.general import PowerConfiguration
from zigpy.zcl.clusters.smartenergy import Metering

import zhaquirks
from zhaquirks.builder.device import QuirkV2Factory
from zhaquirks.lixee import LIXEE, ZLINKY_MANUFACTURER_CLUSTER_ID
from zhaquirks.lixee.zlinky import ZLinkyTICManufacturerCluster, ZLinkyTICMetering

zhaquirks.setup()


def _zlinky_definition():
"""Return the QuirkDefinition registered for the ZLinky_TIC."""
(entry,) = [
entry
for entry in DEVICE_REGISTRY
if isinstance(entry.zha_device_factory, QuirkV2Factory)
and any(
model_info.manufacturer == LIXEE and model_info.model == "ZLinky_TIC"
for model_info in entry.device_match.applies_to
)
]
return entry.zha_device_factory.quirk_definition


def test_zlinky_clusters_replaced(zigpy_device_from_v2_quirk) -> None:
"""Test that the ZLinky_TIC quirk replaces the expected clusters."""
device = zigpy_device_from_v2_quirk(
LIXEE,
"ZLinky_TIC",
cluster_ids={
1: {
Metering.cluster_id: None,
ZLINKY_MANUFACTURER_CLUSTER_ID: None,
}
},
)
endpoint = device.endpoints[1]

assert isinstance(endpoint.smartenergy_metering, ZLinkyTICMetering)
assert isinstance(
endpoint.zlinky_manufacturer_specific, ZLinkyTICManufacturerCluster
)
# Not all firmware variants report it, the quirk adds it unconditionally
assert PowerConfiguration.cluster_id in endpoint.in_clusters


def test_zlinky_tariff_entities() -> None:
"""Test that the tariff and diagnostic sensors are exposed."""
attribute_defs = ZLinkyTICManufacturerCluster.AttributeDefs
definition = _zlinky_definition()

assert {
entity_metadata.attribute_name for entity_metadata in definition.entity_metadata
} == {
attribute_defs.linky_tariff_period.name,
attribute_defs.hist_tariff_option_or_std_supplier_price_schedule_name.name,
attribute_defs.hist_subscribed_power_exceeding_warning.name,
attribute_defs.hist_schedule_peak_hours_off_peak_hours.name,
attribute_defs.linky_status.name,
attribute_defs.linky_mode.name,
}

# Every entity reads from the manufacturer specific cluster on endpoint 1
for entity_metadata in definition.entity_metadata:
assert entity_metadata.cluster_id == ZLINKY_MANUFACTURER_CLUSTER_ID
assert entity_metadata.endpoint_id == 1
3 changes: 0 additions & 3 deletions tests/test_quirks.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,9 +920,6 @@ def check_for_duplicate_cluster_ids(clusters) -> None:
zhaquirks.aduro.adurolightncc.AdurolightNCC,
# add a bunch of output clusters (Zhongxing motion sensor):
zhaquirks.zhongxing.motion.SN10ZW,
# remove Tuya clusters from input and output clusters (ZLinky):
zhaquirks.lixee.zlinky.ZLinkyTICFWV14,
zhaquirks.lixee.zlinky.ZLinkyTICFWV15,
)
],
)
Expand Down
180 changes: 73 additions & 107 deletions zhaquirks/lixee/zlinky.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
"""Quirk for ZLinky_TIC."""

from copy import deepcopy
from typing import Final

from zigpy.profiles import zgp, zha
import zigpy.types as t
from zigpy.zcl.clusters.general import (
Basic,
GreenPowerProxy,
Identify,
Ota,
PowerConfiguration,
Time,
)
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement, MeterIdentification
from zigpy.zcl.clusters.general import PowerConfiguration
from zigpy.zcl.clusters.smartenergy import Metering
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef

from zhaquirks.clusters import CustomCluster
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
from zhaquirks.builder import (
EntityType,
QuirkBuilder,
SensorDeviceClass,
SensorStateClass,
UnitOfElectricCurrent,
)
from zhaquirks.legacy import CustomDevice
from zhaquirks.clusters import CustomCluster
from zhaquirks.lixee import LIXEE, ZLINKY_MANUFACTURER_CLUSTER_ID
from zhaquirks.tuya import TuyaManufCluster


class ZLinkyTICManufacturerCluster(CustomCluster):
Expand Down Expand Up @@ -226,89 +213,68 @@ class ZLinkyTICMetering(CustomCluster, Metering):
_CONSTANT_ATTRIBUTES = {MULTIPLIER: 1, DIVISOR: 1000}


class ZLinkyTIC(CustomDevice):
"""ZLinky_TIC from LiXee."""

signature = {
MODELS_INFO: [(LIXEE, "ZLinky_TIC")],
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
INPUT_CLUSTERS: [
Basic.cluster_id,
Identify.cluster_id,
Metering.cluster_id,
MeterIdentification.cluster_id,
ElectricalMeasurement.cluster_id,
ZLinkyTICManufacturerCluster.cluster_id,
],
OUTPUT_CLUSTERS: [Ota.cluster_id],
},
242: {
PROFILE_ID: zgp.PROFILE_ID,
DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
INPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
INPUT_CLUSTERS: [
Basic.cluster_id,
PowerConfiguration.cluster_id,
Identify.cluster_id,
ZLinkyTICMetering,
MeterIdentification.cluster_id,
ElectricalMeasurement.cluster_id,
ZLinkyTICManufacturerCluster,
],
OUTPUT_CLUSTERS: [Ota.cluster_id],
},
242: {
PROFILE_ID: zgp.PROFILE_ID,
DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
INPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
},
},
}


class ZLinkyTICFWV12(ZLinkyTIC):
"""ZLinky_TIC from LiXee with firmware v12.0 & v13.0."""

signature = deepcopy(ZLinkyTIC.signature)

# Insert PowerConfiguration cluster in signature for devices with firmware v12.0 & v13.0
signature[ENDPOINTS][1][INPUT_CLUSTERS].insert(1, PowerConfiguration.cluster_id)


class ZLinkyTICFWV14(ZLinkyTICFWV12):
"""ZLinky_TIC from LiXee with firmware v14.0+."""

signature = deepcopy(ZLinkyTICFWV12.signature)
replacement = deepcopy(ZLinkyTICFWV12.replacement)

# Insert Time configuration cluster in signature for devices with firmware v14.0+
signature[ENDPOINTS][1][INPUT_CLUSTERS].insert(1, Time.cluster_id)

# Insert Tuya cluster in signature for devices with firmware v14.0+
signature[ENDPOINTS][1][INPUT_CLUSTERS].insert(7, TuyaManufCluster.cluster_id)
signature[ENDPOINTS][1][OUTPUT_CLUSTERS].insert(1, TuyaManufCluster.cluster_id)

replacement[ENDPOINTS][1][INPUT_CLUSTERS].insert(1, Time.cluster_id)


class ZLinkyTICFWV15(ZLinkyTICFWV14):
"""ZLinky_TIC from LiXee with firmware v15.0+."""

signature = deepcopy(ZLinkyTICFWV14.signature)
replacement = deepcopy(ZLinkyTICFWV14.replacement)

signature[ENDPOINTS][1][DEVICE_TYPE] = zha.DeviceType.DIMMABLE_LIGHT
replacement[ENDPOINTS][1][DEVICE_TYPE] = zha.DeviceType.DIMMABLE_LIGHT
(
QuirkBuilder(LIXEE, "ZLinky_TIC")
# Not all firmware variants have a power configuration cluster
.adds(PowerConfiguration.cluster_id, endpoint_id=1)
.replaces(ZLinkyTICMetering, endpoint_id=1)
.replaces(ZLinkyTICManufacturerCluster, endpoint_id=1)
Comment thread
yoda-jm marked this conversation as resolved.
# PTEC: tariff period currently in effect, e.g. "TH.." on the Base tariff or
# "HC.."/"HP.." on the off-peak/peak tariff. Reported in both TIC modes from
# firmware v15. This is what automations need in order to follow the tariff
# period rather than a hardcoded schedule.
.sensor(
attribute_name=ZLinkyTICManufacturerCluster.AttributeDefs.linky_tariff_period.name,
cluster_id=ZLinkyTICManufacturerCluster.cluster_id,
translation_key="linky_tariff_period",
fallback_name="Tariff period",
)
# OPTARIF: tariff option the contract is subscribed to, e.g. "BASE" or
# "HC..". Complements PTEC: one gives the contract, the other the period
# currently in effect.
.sensor(
attribute_name=ZLinkyTICManufacturerCluster.AttributeDefs.hist_tariff_option_or_std_supplier_price_schedule_name.name,
cluster_id=ZLinkyTICManufacturerCluster.cluster_id,
translation_key="linky_tariff_option",
fallback_name="Tariff option",
)
# ADPS: warning raised when the subscribed power is exceeded, in amperes.
# Zero when there is no overload.
.sensor(
attribute_name=ZLinkyTICManufacturerCluster.AttributeDefs.hist_subscribed_power_exceeding_warning.name,
cluster_id=ZLinkyTICManufacturerCluster.cluster_id,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
unit=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=0,
translation_key="linky_subscribed_power_exceeding_warning",
fallback_name="Subscribed power exceeding warning",
)
# HHPHC: off-peak hours schedule group the meter is assigned to. Only
# meaningful once an off-peak tariff is subscribed.
.sensor(
attribute_name=ZLinkyTICManufacturerCluster.AttributeDefs.hist_schedule_peak_hours_off_peak_hours.name,
cluster_id=ZLinkyTICManufacturerCluster.cluster_id,
entity_type=EntityType.DIAGNOSTIC,
translation_key="linky_off_peak_hours_schedule",
fallback_name="Off-peak hours schedule",
)
# MOTDETAT: meter status register, "000000" when nominal.
.sensor(
attribute_name=ZLinkyTICManufacturerCluster.AttributeDefs.linky_status.name,
cluster_id=ZLinkyTICManufacturerCluster.cluster_id,
entity_type=EntityType.DIAGNOSTIC,
translation_key="linky_status",
fallback_name="Meter status",
)
# TIC mode the meter emits: 0 is historical, 1 is standard. Tells users
# which of the hist_/std_ attribute sets their meter populates.
.sensor(
attribute_name=ZLinkyTICManufacturerCluster.AttributeDefs.linky_mode.name,
cluster_id=ZLinkyTICManufacturerCluster.cluster_id,
entity_type=EntityType.DIAGNOSTIC,
translation_key="linky_mode",
fallback_name="TIC mode",
)
.add_to_registry()
)
Loading