Skip to content
Open
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
14 changes: 8 additions & 6 deletions tests/test_schneiderelectric.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

from unittest import mock

import pytest
from zigpy.zcl import foundation
from zigpy.zcl import ClusterType, foundation
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.clusters.smartenergy import Metering

from tests.common import ClusterListener
import zhaquirks
from zhaquirks.schneiderelectric import SE_MANUF_NAME
import zhaquirks.schneiderelectric.outlet

zhaquirks.setup()

Expand Down Expand Up @@ -86,10 +85,13 @@ async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_v2_quir
assert len(cluster_listener.cluster_commands) == 0


@pytest.mark.parametrize("quirk", (zhaquirks.schneiderelectric.outlet.SocketOutlet,))
async def test_schneider_device_temp(zigpy_device_from_quirk, quirk):
async def test_schneider_device_temp(zigpy_device_from_v2_quirk):
"""Test that instant demand is divided by 1000."""
device = zigpy_device_from_quirk(quirk)
device = zigpy_device_from_v2_quirk(
manufacturer=SE_MANUF_NAME,
model="SOCKET/OUTLET/1",
cluster_ids={6: {Metering.cluster_id: ClusterType.Server}},
)

metering_cluster = device.endpoints[6].smartenergy_metering
metering_listener = ClusterListener(metering_cluster)
Expand Down
187 changes: 91 additions & 96 deletions zhaquirks/schneiderelectric/outlet.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,66 @@
"""Schneider Electric (Wiser) Outlet Quirks."""

from zigpy.profiles import zgp, zha
from zigpy.zcl.clusters.general import (
Basic,
GreenPowerProxy,
Groups,
Identify,
OnOff,
Ota,
Scenes,
)
from zigpy.zcl.clusters.homeautomation import Diagnostic, ElectricalMeasurement
from zigpy.zcl.clusters.smartenergy import DeviceManagement, Metering
from typing import Final

from zigpy import types as t
from zigpy.zcl.clusters.smartenergy import Metering
from zigpy.zcl.foundation import BaseAttributeDefs, DataTypeId, ZCLAttributeDef

from zhaquirks.builder import QuirkBuilder
from zhaquirks.clusters import CustomCluster
from zhaquirks.const import (
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
)
from zhaquirks.legacy import CustomDevice
from zhaquirks.schneiderelectric import SE_MANUF_NAME
from zhaquirks.schneiderelectric import SE_MANUF_ID, SE_MANUF_NAME, SEBasic


class SEIndicatorMode(t.enum8):
"""Indicator mode."""

InverseOfOutput = 0x00
FollowsOutput = 0x01
AlwaysOff = 0x02
AlwaysOn = 0x03


class SELocalControlMode(t.enum8):
"""Local control mode."""

Active = 0x00
Inactive = 0x01

class MeteringCluster(CustomCluster, Metering):

class SEOutletConfiguration(CustomCluster):
"""Schneider Electric Outlet Configuration cluster (VISA config, 0xFC04)."""

cluster_id = 0xFC04
name = "SEOutletConfiguration"
ep_attribute = "se_outlet_configuration"

class AttributeDefs(BaseAttributeDefs):
"""Attribute definitions."""

# IndicatorLuminanceLevel: brightness of the indication front LED.
# 0 = 100%, 1 = 80%, 2 = 60%, 3 = 40%, 4 = 20%, 5 = 0%.
se_indicator_luminance_level: Final = ZCLAttributeDef(
id=0x0000,
type=t.uint8_t,
access="rw",
manufacturer_code=SE_MANUF_ID,
)
se_indicator_mode: Final = ZCLAttributeDef(
id=0x0002,
type=SEIndicatorMode,
zcl_type=DataTypeId.uint8,
access="rw",
manufacturer_code=SE_MANUF_ID,
)
se_local_control_mode: Final = ZCLAttributeDef(
id=0x0050,
type=SELocalControlMode,
access="rw",
manufacturer_code=SE_MANUF_ID,
)


class SEMeteringCluster(CustomCluster, Metering):
"""Custom Metering cluster to fix instantaneous demand value multiplied by 1000."""

def _update_attribute(self, attrid, value):
Expand All @@ -35,76 +69,37 @@ def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)


class SocketOutlet(CustomDevice):
"""Schneider Electric Socket outlet WDE002182, WDE002172."""

signature = {
MODELS_INFO: [
(SE_MANUF_NAME, "SOCKET/OUTLET/1"),
(SE_MANUF_NAME, "SOCKET/OUTLET/2"),
],
ENDPOINTS: {
# <SimpleDescriptor endpoint=1 profile=260 device_type=9
# device_version=0
# input_clusters=[0, 3, 4, 5, 6, 1794, 1800, 2820, 2821, 64516] output_clusters=[25]>
6: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET,
INPUT_CLUSTERS: [
Basic.cluster_id,
Identify.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
OnOff.cluster_id,
Metering.cluster_id,
DeviceManagement.cluster_id,
ElectricalMeasurement.cluster_id,
Diagnostic.cluster_id,
0xFC04,
],
OUTPUT_CLUSTERS: [
Ota.cluster_id,
],
},
242: {
PROFILE_ID: zgp.PROFILE_ID,
DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [
GreenPowerProxy.cluster_id,
],
},
},
}

replacement = {
ENDPOINTS: {
6: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET,
INPUT_CLUSTERS: [
Basic.cluster_id,
Identify.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
OnOff.cluster_id,
MeteringCluster,
DeviceManagement.cluster_id,
ElectricalMeasurement.cluster_id,
Diagnostic.cluster_id,
0xFC04,
],
OUTPUT_CLUSTERS: [
Ota.cluster_id,
],
},
242: {
PROFILE_ID: zgp.PROFILE_ID,
DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
INPUT_CLUSTERS: [],
OUTPUT_CLUSTERS: [
GreenPowerProxy.cluster_id,
],
},
},
}
(
QuirkBuilder(SE_MANUF_NAME, "SOCKET/OUTLET/1")
.applies_to(SE_MANUF_NAME, "SOCKET/OUTLET/2")
.replaces(SEBasic, endpoint_id=6)
.replaces(SEMeteringCluster, endpoint_id=6)
.replaces(SEOutletConfiguration, endpoint_id=6)
.number(
attribute_name=SEOutletConfiguration.AttributeDefs.se_indicator_luminance_level.name,
cluster_id=SEOutletConfiguration.cluster_id,
endpoint_id=6,
min_value=0,
max_value=5,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: the scale here is inverted (0 = brightest, 5 = off), which is a little unintuitive as a bare number in the UI. Z2M exposes the same attribute as a named lookup ("100", "80", … "0" percent). Since a number entity was the explicitly requested shape in the earlier review round, this is fine as-is — just noting that an enum with percentage-named members would be self-documenting if you ever want to revisit. The comment on the attribute definition already covers the mapping.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step=1,
translation_key="indicator_luminance_level",
fallback_name="Indicator luminance level",
)
.enum(
attribute_name=SEOutletConfiguration.AttributeDefs.se_indicator_mode.name,
enum_class=SEIndicatorMode,
cluster_id=SEOutletConfiguration.cluster_id,
endpoint_id=6,
translation_key="indicator_mode",
fallback_name="Indicator mode",
)
.enum(
attribute_name=SEOutletConfiguration.AttributeDefs.se_local_control_mode.name,
enum_class=SELocalControlMode,
cluster_id=SEOutletConfiguration.cluster_id,
endpoint_id=6,
translation_key="local_control_mode",
fallback_name="Local control mode",
)
.add_to_registry()
)
Loading