Skip to content

Commit 29fae3a

Browse files
committed
generic_zigbee_sensor_improvement
1 parent 9379c14 commit 29fae3a

33 files changed

+3140
-54
lines changed

drivers/SmartThings/zigbee-sensor/fingerprints.yml

+17-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,20 @@ zigbeeGeneric:
55
server:
66
- 0x0500
77
deviceProfileName: generic-sensor
8-
8+
- id: "generic-battery-sensor"
9+
deviceLabel: "Zigbee Generic Sensor"
10+
deviceIdentifiers:
11+
- 0x0402
12+
clusters:
13+
server:
14+
- 0x0500
15+
- 0x0001
16+
deviceProfileName: generic-sensor
17+
- id: "generic-button-sensor"
18+
deviceLabel: "Zigbee Generic Button"
19+
clusters:
20+
server:
21+
- 0x0500
22+
- 0xFE00 # EZVIZ private cluster
23+
- 0xFE05 # EZVIZ private cluster
24+
deviceProfileName: generic-remote-control
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: generic-motion-illuminance
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: motionSensor
6+
version: 1
7+
- id: illuminanceMeasurement
8+
version: 1
9+
config:
10+
values:
11+
- key: "illuminance.value"
12+
range: [0, 32000]
13+
- id: battery
14+
version: 1
15+
- id: firmwareUpdate
16+
version: 1
17+
- id: refresh
18+
version: 1
19+
categories:
20+
- name: MotionSensor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: generic-remote-control
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: button
6+
version: 1
7+
- id: battery
8+
version: 1
9+
- id: firmwareUpdate
10+
version: 1
11+
- id: refresh
12+
version: 1
13+
categories:
14+
- name: Button
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
local defaults = require "st.zigbee.defaults"
16+
local capabilities = require "st.capabilities"
17+
18+
local is_contact_sensor = function(opts, driver, device)
19+
if device:supports_capability(capabilities.contactSensor) then
20+
return true
21+
end
22+
end
23+
24+
local generic_contact_sensor = {
25+
NAME = "Generic Contact Sensor",
26+
supported_capabilities = {
27+
capabilities.contactSensor
28+
},
29+
can_handle = is_contact_sensor
30+
}
31+
defaults.register_for_default_handlers(generic_contact_sensor, generic_contact_sensor.supported_capabilities)
32+
return generic_contact_sensor
33+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
local capabilities = require "st.capabilities"
16+
local defaults = require "st.zigbee.defaults"
17+
18+
local EZVIZ_PRIVATE_CLUSTER = 0xFE05
19+
local EZVIZ_PRIVATE_ATTRIBUTE = 0x0000
20+
21+
local EZVIZ_MFR = "EZVIZ"
22+
23+
local is_ezviz_button = function(opts, driver, device)
24+
if device:supports_capability(capabilities.button) and device:get_manufacturer() == EZVIZ_MFR then
25+
return true
26+
end
27+
end
28+
29+
-- We need an empty added here to override the added in root init.lua, because we already knows its profile
30+
local device_added = function(self, device)
31+
end
32+
33+
local ezviz_private_cluster_button_handler = function(driver, device, zb_rx)
34+
local event
35+
local additional_fields = {
36+
state_change = true
37+
}
38+
if zb_rx.value == 0x01 then
39+
event = capabilities.button.button.pushed(additional_fields)
40+
elseif zb_rx.value == 0x02 then
41+
event = capabilities.button.button.double(additional_fields)
42+
elseif zb_rx.value == 0x03 then
43+
event = capabilities.button.button.held(additional_fields)
44+
end
45+
if event ~= nil then
46+
device:emit_event(event)
47+
end
48+
end
49+
50+
local ezviz_button_handler = {
51+
NAME = "Ezviz Button",
52+
supported_capabilities = {
53+
capabilities.battery,
54+
capabilities.button,
55+
capabilities.refresh
56+
},
57+
zigbee_handlers = {
58+
attr = {
59+
[EZVIZ_PRIVATE_CLUSTER] = {
60+
[EZVIZ_PRIVATE_ATTRIBUTE] = ezviz_private_cluster_button_handler
61+
}
62+
}
63+
},
64+
lifecycle_handlers = {
65+
added = device_added
66+
},
67+
can_handle = is_ezviz_button
68+
}
69+
defaults.register_for_default_handlers(ezviz_button_handler, ezviz_button_handler.supported_capabilities)
70+
return ezviz_button_handler

drivers/SmartThings/zigbee-sensor/src/init.lua

+23-48
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ local capabilities = require "st.capabilities"
1919
local constants = require "st.zigbee.constants"
2020
local IasZoneType = require "st.zigbee.generated.types.IasZoneType"
2121
local device_management = require "st.zigbee.device_management"
22+
local PowerConfiguration = clusters.PowerConfiguration
2223

2324
local CONTACT_SWITCH = IasZoneType.CONTACT_SWITCH
2425
local MOTION_SENSOR = IasZoneType.MOTION_SENSOR
2526
local WATER_SENSOR = IasZoneType.WATER_SENSOR
27+
local REMOTE_CONTROL = IasZoneType.REMOTE_CONTROL
2628

2729
local ZIGBEE_GENERIC_SENSOR_PROFILE = "generic-sensor"
2830
local ZIGBEE_GENERIC_CONTACT_SENSOR_PROFILE = "generic-contact-sensor"
2931
local ZIGBEE_GENERIC_MOTION_SENSOR_PROFILE = "generic-motion-sensor"
3032
local ZIGBEE_GENERIC_WATERLEAK_SENSOR_PROFILE = "generic-waterleak-sensor"
33+
local ZIGBEE_GENERIC_MOTION_ILLUMINANCE_PROFILE = "generic-motion-illuminance"
34+
local ZIGBEE_GENERIC_REMOTE_CONTROL_PROFILE = "generic-remote-control"
3135

3236
local ZONETYPE = "ZoneType"
3337
local IASZone = clusters.IASZone
@@ -42,6 +46,7 @@ local do_configure = function(self, device)
4246
device:configure()
4347
device:send(device_management.build_bind_request(device, IASZone.ID, self.environment_info.hub_zigbee_eui))
4448
device:send(IASZone.attributes.ZoneStatus:configure_reporting(device, 30, 300, 1))
49+
device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device))
4550
end
4651

4752
local function info_changed(driver, device, event, args)
@@ -55,12 +60,18 @@ local function update_profile(device, zone_type)
5560
local profile = ZIGBEE_GENERIC_SENSOR_PROFILE
5661
if zone_type == CONTACT_SWITCH then
5762
profile = ZIGBEE_GENERIC_CONTACT_SENSOR_PROFILE
58-
elseif zone_type == MOTION_SENSOR then
59-
profile = ZIGBEE_GENERIC_MOTION_SENSOR_PROFILE
63+
elseif zone_type == REMOTE_CONTROL then
64+
profile = ZIGBEE_GENERIC_REMOTE_CONTROL_PROFILE
6065
elseif zone_type == WATER_SENSOR then
6166
profile = ZIGBEE_GENERIC_WATERLEAK_SENSOR_PROFILE
67+
elseif zone_type == MOTION_SENSOR then
68+
profile = ZIGBEE_GENERIC_MOTION_SENSOR_PROFILE
69+
for _, ep in ipairs(device.zigbee_endpoints) do
70+
if device:supports_server_cluster(clusters.IlluminanceMeasurement.ID, ep.id) then
71+
profile = ZIGBEE_GENERIC_MOTION_ILLUMINANCE_PROFILE
72+
end
73+
end
6274
end
63-
6475
device:try_update_metadata({profile = profile})
6576
end
6677

@@ -70,44 +81,6 @@ local ias_zone_type_attr_handler = function (driver, device, attr_val)
7081
update_profile(device, attr_val.value)
7182
end
7283

73-
-- since we don't have button devices using IASZone, the driver here is remaining to be updated
74-
local generate_event_from_zone_status = function(driver, device, zone_status, zb_rx)
75-
local type = device:get_field(ZONETYPE)
76-
local event
77-
if type == CONTACT_SWITCH then
78-
if zone_status:is_alarm1_set() or zone_status:is_alarm2_set() then
79-
event = capabilities.contactSensor.contact.open()
80-
else
81-
event = capabilities.contactSensor.contact.closed()
82-
end
83-
elseif type == MOTION_SENSOR then
84-
if zone_status:is_alarm1_set() or zone_status:is_alarm2_set() then
85-
event = capabilities.motionSensor.motion.active()
86-
else
87-
event = capabilities.motionSensor.motion.inactive()
88-
end
89-
elseif type == WATER_SENSOR then
90-
if zone_status:is_alarm1_set() then
91-
event = capabilities.waterSensor.water.wet()
92-
else
93-
event = capabilities.waterSensor.water.dry()
94-
end
95-
end
96-
if event ~= nil then
97-
device:emit_event_for_endpoint(
98-
zb_rx.address_header.src_endpoint.value,
99-
event)
100-
end
101-
end
102-
103-
local ias_zone_status_attr_handler = function(driver, device, zone_status, zb_rx)
104-
generate_event_from_zone_status(driver, device, zone_status, zb_rx)
105-
end
106-
107-
local ias_zone_status_change_handler = function(driver, device, zb_rx)
108-
generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx)
109-
end
110-
11184
local zigbee_generic_sensor_template = {
11285
supported_capabilities = {
11386
capabilities.battery,
@@ -117,13 +90,7 @@ local zigbee_generic_sensor_template = {
11790
zigbee_handlers = {
11891
attr = {
11992
[IASZone.ID] = {
120-
[IASZone.attributes.ZoneType.ID] = ias_zone_type_attr_handler,
121-
[IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler
122-
}
123-
},
124-
cluster = {
125-
[IASZone.ID] = {
126-
[IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler
93+
[IASZone.attributes.ZoneType.ID] = ias_zone_type_attr_handler
12794
}
12895
}
12996
},
@@ -132,6 +99,14 @@ local zigbee_generic_sensor_template = {
13299
doConfigure = do_configure,
133100
infoChanged = info_changed
134101
},
102+
sub_drivers = {
103+
require("contact"),
104+
require("motion"),
105+
require("waterleak"),
106+
require("motion-illuminance"),
107+
require("meian"),
108+
require("ezviz")
109+
},
135110
ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE
136111
}
137112

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
local clusters = require "st.zigbee.zcl.clusters"
16+
local capabilities = require "st.capabilities"
17+
local read_attribute = require "st.zigbee.zcl.global_commands.read_attribute"
18+
local constants = require "st.zigbee.constants"
19+
local zcl_messages = require "st.zigbee.zcl"
20+
local data_types = require "st.zigbee.data_types"
21+
local messages = require "st.zigbee.messages"
22+
local defaults = require "st.zigbee.defaults"
23+
24+
local IASZone = clusters.IASZone
25+
local IASACE = clusters.IASACE
26+
27+
local TUYA_MFR_HEADER = "_TZ"
28+
29+
local is_meian_sos_button = function(opts, driver, device)
30+
if device:supports_capability(capabilities.button) and string.sub(device:get_manufacturer(),1,3) == TUYA_MFR_HEADER then
31+
return true
32+
end
33+
end
34+
35+
local function read_attribute_function(device, cluster_id, attr_id)
36+
local read_body = read_attribute.ReadAttribute( attr_id )
37+
local zclh = zcl_messages.ZclHeader({
38+
cmd = data_types.ZCLCommandId(read_attribute.ReadAttribute.ID)
39+
})
40+
local addrh = messages.AddressHeader(
41+
constants.HUB.ADDR,
42+
constants.HUB.ENDPOINT,
43+
device:get_short_address(),
44+
device:get_endpoint(cluster_id),
45+
constants.HA_PROFILE_ID,
46+
cluster_id
47+
)
48+
local message_body = zcl_messages.ZclMessageBody({
49+
zcl_header = zclh,
50+
zcl_body = read_body
51+
})
52+
return messages.ZigbeeMessageTx({
53+
address_header = addrh,
54+
body = message_body
55+
})
56+
end
57+
58+
local function info_changed(driver, device, event, args)
59+
if device.profile.id ~= args.old_st_store.profile.id then
60+
local magic_spell = {0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xfffe}
61+
device:send(IASZone.attributes.ZoneStatus:read(device))
62+
device:send(read_attribute_function(device, clusters.Basic.ID, magic_spell))
63+
end
64+
end
65+
66+
local ias_ace_emergency_handler = function(driver, device)
67+
device:emit_event(capabilities.button.button.pushed({ state_change = true }))
68+
end
69+
70+
local meian_sos_button_handler = {
71+
NAME = "Meian Sos Button",
72+
supported_capabilities = {
73+
capabilities.battery,
74+
capabilities.button,
75+
capabilities.refresh
76+
},
77+
lifecycle_handlers = {
78+
infoChanged = info_changed
79+
},
80+
zigbee_handlers = {
81+
cluster = {
82+
[IASACE.ID] = {
83+
[IASACE.server.commands.Emergency.ID] = ias_ace_emergency_handler
84+
}
85+
}
86+
},
87+
can_handle = is_meian_sos_button
88+
}
89+
defaults.register_for_default_handlers(meian_sos_button_handler, meian_sos_button_handler.supported_capabilities)
90+
return meian_sos_button_handler

0 commit comments

Comments
 (0)