Skip to content

Add support for Develco/frient Smart Plugs (SPLZB-131/132/134/137/141/142/144/147), Smart Cables (SMRZB-143/153) and Smart DIN Relays (SMRZB-332/342)#4871

Open
marcintyminski wants to merge 3 commits intozigpy:devfrom
marcintyminski:smart-plugs-quirk
Open

Add support for Develco/frient Smart Plugs (SPLZB-131/132/134/137/141/142/144/147), Smart Cables (SMRZB-143/153) and Smart DIN Relays (SMRZB-332/342)#4871
marcintyminski wants to merge 3 commits intozigpy:devfrom
marcintyminski:smart-plugs-quirk

Conversation

@marcintyminski
Copy link
Copy Markdown

Proposed change

The quirk has been made to extend functionality of the following frient/Develco Smart Plugs, Smart Cables and Smart DIN Relays:

  • frient Smart Plug Mini with model no. SPLZB-131
  • frient Smart Plug Mini with model no. SPLZB-132
  • frient Smart Plug Mini UK with model no. SPLZB-134
  • frient Smart Plug Mini with model no. SPLZB-137
  • frient Smart Plug Mini 2 with model no. SPLZB-141
  • frient Smart Plug Mini 2 with model no. SPLZB-142
  • frient Smart Plug Mini 2 UK with model no. SPLZB-144
  • frient Smart Plug Mini 2 with model no. SPLZB-147
  • frient Smart Cable with model no. SMRZB-143
  • frient Smart Cable 2 with model no. SMRZB-153
  • frient Smart DIN Relay with model no. SMRZB-332
  • frient Smart DIN Relay 2 with model no. SMRZB-342
  • Develco Smart Plug Mini with model no. SPLZB-131
  • Develco Smart Plug Mini with model no. SPLZB-132
  • Develco Smart Plug Mini UK with model no. SPLZB-134
  • Develco Smart Plug Mini with model no. SPLZB-137
  • Develco Smart Plug Mini 2 with model no. SPLZB-141
  • Develco Smart Plug Mini 2 with model no. SPLZB-142
  • Develco Smart Plug Mini 2 UK with model no. SPLZB-144
  • Develco Smart Plug Mini 2 with model no. SPLZB-147
  • Develco Smart Cable with model no. SMRZB-143
  • Develco Smart Cable 2 with model no. SMRZB-153
  • Develco Smart DIN Relay with model no. SMRZB-332
  • Develco Smart DIN Relay 2 with model no. SMRZB-342

The quirk contains following changes:

  • Add delayed turn on/off functionality
  • Add return to state entity to track the last sent command to turn on/off with delay

Additional information

image

Device diagnostics

zha-01KJYXWD9VV3FMNDJEFN115MC4-frient A_S SMRZB-153-59b661ecb0fc7ef52e3630f969e79742.json
zha-01KJYXWD9VV3FMNDJEFN115MC4-frient A_S SMRZB-342-c9ca9838109ed505f89b8687390fccfc.json
zha-01KJYXWD9VV3FMNDJEFN115MC4-frient A_S SPLZB-132-54d2a1249af9626f191030cccc09a47d.json

Checklist

  • The changes are tested and work correctly
  • pre-commit checks pass / the code has been formatted using Black
  • Tests have been added to verify that the new code works
  • Device diagnostics data has been attached

Copilot AI review requested due to automatic review settings March 27, 2026 10:52
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.06%. Comparing base (380135d) to head (07b460a).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4871      +/-   ##
==========================================
+ Coverage   93.04%   93.06%   +0.02%     
==========================================
  Files         397      397              
  Lines       13248    13290      +42     
==========================================
+ Hits        12327    12369      +42     
  Misses        921      921              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends ZHA-Quirks support for multiple Develco/frient smart plugs/cables/DIN relays by adding vendor-specific delayed on/off controls and exposing additional diagnostic entities via a custom OnOff cluster replacement.

Changes:

  • Add a VendorOnOff custom cluster that maps writes to vendor “safe mode” commands and seeds default attribute values.
  • Expand model/manufacturer coverage for SPLZB/SMRZB device variants and add new HA entities (delay numbers + diagnostic binary sensor).
  • Add unit tests validating cluster behavior, request construction, and v2 entity metadata/disabled-default-entity registration.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
zhaquirks/develco/power_plug.py Adds custom OnOff cluster and v2 quirk builder metadata/entities for the supported device family.
tests/test_develco.py Adds tests for the new custom cluster behavior and v2 registry/entity metadata expectations.

Comment on lines +16 to +18
from zigpy.zcl.clusters.general import DeviceTemperature, OnOff
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zigpy.zcl.foundation import ZCLAttributeDef
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Metering is imported but not used in this module, which will fail linting (ruff/pyflakes F401). Remove the unused import, or add the metering-related logic/entities that were intended to use it.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to 93
mode_value = None

if self.AttributeDefs.mode_on_value.id in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_on_value.id)
await self._send_safe_mode(0x01, mode_value)
self._update_attribute(self.AttributeDefs.mode_on_value.id, mode_value)
elif self.AttributeDefs.mode_off_value.id in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_off_value.id)
await self._send_safe_mode(0x00, mode_value)
self._update_attribute(self.AttributeDefs.mode_off_value.id, mode_value)
elif self.AttributeDefs.mode_on_value.name in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_on_value.name)
await self._send_safe_mode(0x01, mode_value)
self._update_attribute(self.AttributeDefs.mode_on_value.id, mode_value)
elif self.AttributeDefs.mode_off_value.name in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_off_value.name)
await self._send_safe_mode(0x00, mode_value)
self._update_attribute(self.AttributeDefs.mode_off_value.id, mode_value)

if attributes_copy:
return await super().write_attributes(attributes_copy, **kwargs)

return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]


Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

write_attributes() only handles one of the vendor delay attributes because of the if/elif chain. If a caller writes both mode_on_value and mode_off_value in the same call, the second attribute will fall through to super().write_attributes(...) and be sent as a real Zigbee write (likely unsupported). Consider normalizing keys first and processing both vendor attributes (and their ZCLAttributeDef key form) before delegating the remaining attributes to super().

Suggested change
mode_value = None
if self.AttributeDefs.mode_on_value.id in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_on_value.id)
await self._send_safe_mode(0x01, mode_value)
self._update_attribute(self.AttributeDefs.mode_on_value.id, mode_value)
elif self.AttributeDefs.mode_off_value.id in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_off_value.id)
await self._send_safe_mode(0x00, mode_value)
self._update_attribute(self.AttributeDefs.mode_off_value.id, mode_value)
elif self.AttributeDefs.mode_on_value.name in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_on_value.name)
await self._send_safe_mode(0x01, mode_value)
self._update_attribute(self.AttributeDefs.mode_on_value.id, mode_value)
elif self.AttributeDefs.mode_off_value.name in attributes_copy:
mode_value = attributes_copy.pop(self.AttributeDefs.mode_off_value.name)
await self._send_safe_mode(0x00, mode_value)
self._update_attribute(self.AttributeDefs.mode_off_value.id, mode_value)
if attributes_copy:
return await super().write_attributes(attributes_copy, **kwargs)
return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]
handled_vendor_attr = False
# Normalize keys and handle all occurrences of the vendor attributes,
# regardless of whether they are addressed by ID, name, or ZCLAttributeDef.
for key in list(attributes_copy.keys()):
attr_def: ZCLAttributeDef | None = None
if isinstance(key, ZCLAttributeDef):
attr_def = key
elif key == self.AttributeDefs.mode_on_value.id or key == self.AttributeDefs.mode_on_value.name:
attr_def = self.AttributeDefs.mode_on_value
elif key == self.AttributeDefs.mode_off_value.id or key == self.AttributeDefs.mode_off_value.name:
attr_def = self.AttributeDefs.mode_off_value
if attr_def is None:
continue
if (
attr_def is self.AttributeDefs.mode_on_value
or attr_def.id == self.AttributeDefs.mode_on_value.id
):
mode_value = attributes_copy.pop(key)
await self._send_safe_mode(0x01, mode_value)
self._update_attribute(self.AttributeDefs.mode_on_value.id, mode_value)
handled_vendor_attr = True
elif (
attr_def is self.AttributeDefs.mode_off_value
or attr_def.id == self.AttributeDefs.mode_off_value.id
):
mode_value = attributes_copy.pop(key)
await self._send_safe_mode(0x00, mode_value)
self._update_attribute(self.AttributeDefs.mode_off_value.id, mode_value)
handled_vendor_attr = True
if attributes_copy:
return await super().write_attributes(attributes_copy, **kwargs)
if handled_vendor_attr:
# Only vendor attributes were written; report success without
# issuing any underlying ZCL write.
return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]
# No attributes to handle; fall back to the base implementation.
return await super().write_attributes(attributes_copy, **kwargs)

Copilot uses AI. Check for mistakes.
mode_on_value: Final = ZCLAttributeDef(id=0x8102, type=t.uint8_t, access="w")
mode_off_value: Final = ZCLAttributeDef(id=0x8103, type=t.uint8_t, access="w")
return_to_state: Final = ZCLAttributeDef(
id=0x8101, type=t.Bool, access="r", manufacturer_code=MANUFACTURER_CODE
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

return_to_state is defined with access="r", but the quirk configures attribute reporting for it via ReportingConfig. If this attribute is meant to be reportable, mark it as "rp" (or "rwp" if writable); otherwise remove the reporting configuration to avoid attempting to configure reporting on a non-reportable attribute.

Suggested change
id=0x8101, type=t.Bool, access="r", manufacturer_code=MANUFACTURER_CODE
id=0x8101, type=t.Bool, access="rp", manufacturer_code=MANUFACTURER_CODE

Copilot uses AI. Check for mistakes.
.prevent_default_entity_creation(
endpoint_id=2,
cluster_id=DeviceTemperature.cluster_id,
function=lambda entity: entity.__class__.__name__ == "DeviceTemperature",
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Using entity.__class__.__name__ == "DeviceTemperature" to identify the default temperature entity is brittle (class names can change upstream). If the intent is to remove the default temperature entity entirely on endpoint 2, prefer calling prevent_default_entity_creation(endpoint_id=2, cluster_id=DeviceTemperature.cluster_id) without a predicate, or match on a stable property like translation_key/unique_id if you only want to suppress a specific one.

Suggested change
function=lambda entity: entity.__class__.__name__ == "DeviceTemperature",

Copilot uses AI. Check for mistakes.
@TheJulianJES TheJulianJES added the manufacturer This request was made by the device's manufacturer label Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manufacturer This request was made by the device's manufacturer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants