Skip to content

Commit ed488a6

Browse files
authored
Initial version, adding support for frient Motion Sensor/Pro (#2135)
* Initial version, adding support for frient Motion Sensor/Pro * Fix 1 according to test results * Fix 2: Deprecated test. These devices are now covered by test_frient_motion_sensor.lua and test_frient_motion_sensor_pro.lua * Fix 3 - restoring previous format * Fix 4 - according to feedback
1 parent a8d39ee commit ed488a6

File tree

8 files changed

+1005
-223
lines changed

8 files changed

+1005
-223
lines changed

drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,15 @@ zigbeeManufacturer:
159159
model: VMS_ADUROLIGHT
160160
deviceProfileName: motion-battery
161161
- id: frientA/S/140
162-
deviceLabel: frient Motion Sensor
162+
deviceLabel: frient Motion Sensor Pro
163163
manufacturer: frient A/S
164164
model: MOSZB-140
165-
deviceProfileName: motion-temp-battery
165+
deviceProfileName: frient-motion-temp-illuminance-tamper-battery
166166
- id: frientA/S/141
167167
deviceLabel: frient Motion Sensor
168168
manufacturer: frient A/S
169169
model: MOSZB-141
170-
deviceProfileName: motion-battery
170+
deviceProfileName: frient-motion-battery
171171
- id: Compacta/ZBMS3-1
172172
deviceLabel: Smartenit Motion Sensor
173173
manufacturer: Compacta
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: frient-motion-battery
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: motionSensor
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: MotionSensor
15+
preferences:
16+
- title: "Motion Turn-Off Delay (s)"
17+
name: occupiedToUnoccupiedD
18+
description: "Delay in seconds to report after no motion is detected"
19+
required: false
20+
preferenceType: integer
21+
definition:
22+
minimum: 0
23+
maximum: 65534
24+
default: 240
25+
- title: "Motion Turn-On Delay (s)"
26+
name: unoccupiedToOccupiedD
27+
description: "Delay in seconds to report after motion is detected"
28+
required: false
29+
preferenceType: integer
30+
definition:
31+
minimum: 0
32+
maximum: 65534
33+
default: 0
34+
- title: "Movement Threshold in Turn-On Delay"
35+
name: unoccupiedToOccupiedT
36+
description: "Number of movements to detect before reporting motion during the Motion Turn-On Delay"
37+
required: false
38+
preferenceType: integer
39+
definition:
40+
minimum: 1
41+
maximum: 254
42+
default: 1
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: frient-motion-temp-illuminance-tamper-battery
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: motionSensor
6+
version: 1
7+
- id: temperatureMeasurement
8+
version: 1
9+
- id: illuminanceMeasurement
10+
version: 1
11+
- id: battery
12+
version: 1
13+
- id: tamperAlert
14+
version: 1
15+
- id: firmwareUpdate
16+
version: 1
17+
- id: refresh
18+
version: 1
19+
categories:
20+
- name: MotionSensor
21+
preferences:
22+
- preferenceId: tempOffset
23+
explicit: true
24+
- title: "Temperature Sensitivity (°C)"
25+
name: temperatureSensitivity
26+
description: "Minimum change in temperature to report"
27+
required: false
28+
preferenceType: number
29+
definition:
30+
minimum: 0.1
31+
maximum: 2.0
32+
default: 1.0
33+
- title: "Motion Turn-Off Delay (s)"
34+
name: occupiedToUnoccupiedD
35+
description: "Delay in seconds to report after no motion is detected"
36+
required: false
37+
preferenceType: integer
38+
definition:
39+
minimum: 0
40+
maximum: 65534
41+
default: 240
42+
- title: "Motion Turn-On Delay (s)"
43+
name: unoccupiedToOccupiedD
44+
description: "Delay in seconds to report after motion is detected"
45+
required: false
46+
preferenceType: integer
47+
definition:
48+
minimum: 0
49+
maximum: 65534
50+
default: 0
51+
- title: "Movement Threshold in Turn-On Delay"
52+
name: unoccupiedToOccupiedT
53+
description: "Number of movements to detect before reporting motion during the Motion Turn-On Delay"
54+
required: false
55+
preferenceType: integer
56+
definition:
57+
minimum: 1
58+
maximum: 254
59+
default: 1

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

Lines changed: 185 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,72 +12,214 @@
1212
-- See the License for the specific language governing permissions and
1313
-- limitations under the License.
1414

15-
-- ZCL
15+
local battery_defaults = require "st.zigbee.defaults.battery_defaults"
16+
local capabilities = require "st.capabilities"
1617
local zcl_clusters = require "st.zigbee.zcl.clusters"
17-
local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement
18+
local device_management = require "st.zigbee.device_management"
19+
20+
local IASZone = zcl_clusters.IASZone
21+
local IlluminanceMeasurement = zcl_clusters.IlluminanceMeasurement
1822
local OccupancySensing = zcl_clusters.OccupancySensing
1923
local PowerConfiguration = zcl_clusters.PowerConfiguration
20-
local battery_defaults = require "st.zigbee.defaults.battery_defaults"
24+
local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement
2125

22-
local capabilities = require "st.capabilities"
26+
local BATTERY_MIN_VOLTAGE = 2.3
27+
local BATTERY_MAX_VOLTAGE = 3.0
28+
local DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY = 240
29+
local DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY = 0
30+
local DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD = 0
2331

24-
local FRIENT_TEMP_CONFIG = {
25-
minimum_interval = 30,
26-
maximum_interval = 300,
27-
reportable_change = 100,
28-
endpoint = 0x26
29-
}
32+
local OCCUPANCY_ENDPOINT = 0x22
33+
local TAMPER_ENDPOINT = 0x23
34+
local POWER_CONFIGURATION_ENDPOINT = 0x23
35+
local TEMPERATURE_ENDPOINT = 0x26
36+
local ILLUMINANCE_ENDPOINT = 0x27
3037

31-
local FRIENT_BATTERY_CONFIG = {
32-
minimum_interval = 30,
33-
maximum_interval = 21600,
34-
reportable_change = 1,
35-
endpoint = 0x23
38+
local FRIENT_DEVICE_FINGERPRINTS = {
39+
{ mfr = "frient A/S", model = "MOSZB-140"},
40+
{ mfr = "frient A/S", model = "MOSZB-141"}
3641
}
3742

43+
local function can_handle_frient_motion_sensor(opts, driver, device)
44+
for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do
45+
if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then
46+
return true
47+
end
48+
end
49+
return false
50+
end
51+
3852
local function occupancy_attr_handler(driver, device, occupancy, zb_rx)
39-
device:emit_event(
40-
occupancy.value == 1 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()
41-
)
53+
device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive())
54+
end
55+
56+
local function generate_event_from_zone_status(driver, device, zone_status, zb_rx)
57+
device:emit_event(zone_status:is_tamper_set() and capabilities.tamperAlert.tamper.detected() or capabilities.tamperAlert.tamper.clear())
58+
end
59+
60+
local function ias_zone_status_attr_handler(driver, device, attr_val, zb_rx)
61+
generate_event_from_zone_status(driver, device, attr_val, zb_rx)
62+
end
63+
64+
local function ias_zone_status_change_handler(driver, device, zb_rx)
65+
generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx)
66+
end
67+
68+
69+
local CONFIGURATIONS = {
70+
[OCCUPANCY_ENDPOINT] = {
71+
cluster = OccupancySensing.ID,
72+
attribute = OccupancySensing.attributes.Occupancy.ID,
73+
data_type = OccupancySensing.attributes.Occupancy.base_type,
74+
minimum_interval = 0,
75+
maximum_interval = 3600,
76+
endpoint = OCCUPANCY_ENDPOINT
77+
},
78+
[TAMPER_ENDPOINT] = {
79+
cluster = IASZone.ID,
80+
attribute = IASZone.attributes.ZoneStatus.ID,
81+
minimum_interval = 30,
82+
maximum_interval = 300,
83+
data_type = IASZone.attributes.ZoneStatus.base_type,
84+
reportable_change = 1,
85+
endpoint = TAMPER_ENDPOINT
86+
},
87+
[TEMPERATURE_ENDPOINT] = {
88+
cluster = TemperatureMeasurement.ID,
89+
attribute = TemperatureMeasurement.attributes.MeasuredValue.ID,
90+
minimum_interval = 30,
91+
maximum_interval = 3600,
92+
data_type = TemperatureMeasurement.attributes.MeasuredValue.base_type,
93+
reportable_change = 10,
94+
endpoint = TEMPERATURE_ENDPOINT
95+
},
96+
[ILLUMINANCE_ENDPOINT] = {
97+
cluster = IlluminanceMeasurement.ID,
98+
attribute = IlluminanceMeasurement.attributes.MeasuredValue.ID,
99+
data_type = IlluminanceMeasurement.attributes.MeasuredValue.base_type,
100+
minimum_interval = 10,
101+
maximum_interval = 3600,
102+
reportable_change = 0x2711,
103+
endpoint = ILLUMINANCE_ENDPOINT
104+
}
105+
}
106+
107+
local function device_init(driver, device)
108+
battery_defaults.build_linear_voltage_init(BATTERY_MIN_VOLTAGE, BATTERY_MAX_VOLTAGE)(driver, device)
109+
110+
local attribute
111+
attribute = CONFIGURATIONS[OCCUPANCY_ENDPOINT]
112+
-- binding is directly triggered for specific endpoint in do_configure
113+
device:add_monitored_attribute(attribute)
114+
115+
if device:supports_capability_by_id(capabilities.temperatureMeasurement.ID) then
116+
attribute = CONFIGURATIONS[TEMPERATURE_ENDPOINT]
117+
device:add_configured_attribute(attribute)
118+
device:add_monitored_attribute(attribute)
119+
end
120+
if device:supports_capability_by_id(capabilities.illuminanceMeasurement.ID) then
121+
attribute = CONFIGURATIONS[ILLUMINANCE_ENDPOINT]
122+
device:add_configured_attribute(attribute)
123+
device:add_monitored_attribute(attribute)
124+
end
125+
if device:supports_capability_by_id(capabilities.tamperAlert.ID) then
126+
attribute = CONFIGURATIONS[TAMPER_ENDPOINT]
127+
device:add_configured_attribute(attribute)
128+
device:add_monitored_attribute(attribute)
129+
end
42130
end
43131

132+
local function device_added(driver, device)
133+
device:emit_event(capabilities.motionSensor.motion.inactive())
134+
if device:supports_capability_by_id(capabilities.tamperAlert.ID) then
135+
device:emit_event(capabilities.tamperAlert.tamper.clear())
136+
end
137+
end
138+
139+
local function do_refresh(driver, device)
140+
device:send(OccupancySensing.attributes.Occupancy:read(device):to_endpoint(OCCUPANCY_ENDPOINT))
141+
device:send(PowerConfiguration.attributes.BatteryVoltage:read(device):to_endpoint(POWER_CONFIGURATION_ENDPOINT))
142+
143+
if device:supports_capability_by_id(capabilities.temperatureMeasurement.ID) then
144+
device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(TEMPERATURE_ENDPOINT))
145+
end
146+
if device:supports_capability_by_id(capabilities.illuminanceMeasurement.ID) then
147+
device:send(IlluminanceMeasurement.attributes.MeasuredValue:read(device):to_endpoint(ILLUMINANCE_ENDPOINT))
148+
end
149+
if device:supports_capability_by_id(capabilities.tamperAlert.ID) then
150+
device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(TAMPER_ENDPOINT))
151+
end
152+
end
153+
154+
44155
local function do_configure(driver, device)
45156
device:configure()
46-
device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting(
47-
device,
48-
FRIENT_BATTERY_CONFIG.minimum_interval,
49-
FRIENT_BATTERY_CONFIG.maximum_interval,
50-
FRIENT_BATTERY_CONFIG.reportable_change
51-
):to_endpoint(FRIENT_BATTERY_CONFIG.endpoint))
52-
device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(
53-
device,
54-
FRIENT_TEMP_CONFIG.minimum_interval,
55-
FRIENT_TEMP_CONFIG.maximum_interval,
56-
FRIENT_TEMP_CONFIG.reportable_change
57-
):to_endpoint(FRIENT_TEMP_CONFIG.endpoint))
157+
device:send(device_management.build_bind_request(
158+
device,
159+
zcl_clusters.OccupancySensing.ID,
160+
driver.environment_info.hub_zigbee_eui,
161+
OCCUPANCY_ENDPOINT
162+
))
163+
164+
device:send(OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(device, tonumber(DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY)):to_endpoint(OCCUPANCY_ENDPOINT))
165+
device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(device, tonumber(DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY)):to_endpoint(OCCUPANCY_ENDPOINT))
166+
device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(device, tonumber(DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD)):to_endpoint(OCCUPANCY_ENDPOINT))
167+
device:send(OccupancySensing.attributes.Occupancy:configure_reporting(device, 0, 3600):to_endpoint(OCCUPANCY_ENDPOINT))
168+
169+
device.thread:call_with_delay(5, function()
170+
do_refresh(driver, device)
171+
end)
58172
end
59173

60-
local function added_handler(driver, device)
61-
device:refresh()
174+
local function info_changed(driver, device, event, args)
175+
for name, value in pairs(device.preferences) do
176+
if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then
177+
if (name == "temperatureSensitivity") then
178+
local input = device.preferences.temperatureSensitivity
179+
local temperatureSensitivity = math.floor(input * 100 + 0.5)
180+
device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, temperatureSensitivity):to_endpoint(TEMPERATURE_ENDPOINT))
181+
elseif (name == "occupiedToUnoccupiedD") then
182+
local occupiedToUnoccupiedDelay = device.preferences.occupiedToUnoccupiedD or DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY
183+
device:send(OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(device, occupiedToUnoccupiedDelay):to_endpoint(OCCUPANCY_ENDPOINT))
184+
elseif (name == "unoccupiedToOccupiedD") then
185+
local occupiedToUnoccupiedD = device.preferences.unoccupiedToOccupiedD or DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY
186+
device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(device, occupiedToUnoccupiedD):to_endpoint(OCCUPANCY_ENDPOINT))
187+
elseif (name == "unoccupiedToOccupiedT") then
188+
local unoccupiedToOccupiedThreshold = device.preferences.unoccupiedToOccupiedT or DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD
189+
device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(device,unoccupiedToOccupiedThreshold):to_endpoint(OCCUPANCY_ENDPOINT))
190+
end
191+
end
192+
end
62193
end
63194

64-
local frient_driver = {
65-
NAME = "Frient Sensor",
195+
local frient_motion_driver = {
196+
NAME = "frient motion driver",
197+
lifecycle_handlers = {
198+
added = device_added,
199+
doConfigure = do_configure,
200+
init = device_init,
201+
infoChanged = info_changed
202+
},
66203
zigbee_handlers = {
204+
cluster = {
205+
[IASZone.ID] = {
206+
[IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler
207+
}
208+
},
67209
attr = {
68210
[OccupancySensing.ID] = {
69211
[OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler
212+
},
213+
[IASZone.ID] = {
214+
[IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler
70215
}
71216
}
72217
},
73-
lifecycle_handlers = {
74-
init = battery_defaults.build_linear_voltage_init(2.3, 3.0),
75-
added = added_handler,
76-
doConfigure = do_configure
218+
capability_handlers = {
219+
[capabilities.refresh.ID] = {
220+
[capabilities.refresh.commands.refresh.NAME] = do_refresh
221+
}
77222
},
78-
can_handle = function(opts, driver, device, ...)
79-
return device:get_manufacturer() == "frient A/S"
80-
end
223+
can_handle = can_handle_frient_motion_sensor
81224
}
82-
83-
return frient_driver
225+
return frient_motion_driver

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ local zigbee_motion_driver = {
9090
capabilities.relativeHumidityMeasurement,
9191
capabilities.battery,
9292
capabilities.presenceSensor,
93-
capabilities.contactSensor
93+
capabilities.contactSensor,
94+
capabilities.illuminanceMeasurement
9495
},
9596
zigbee_handlers = {
9697
attr = {
@@ -132,7 +133,6 @@ local zigbee_motion_driver = {
132133
},
133134
ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE
134135
}
135-
136136
defaults.register_for_default_handlers(zigbee_motion_driver,
137137
zigbee_motion_driver.supported_capabilities, {native_capability_attrs_enabled = true})
138138
local motion = ZigbeeDriver("zigbee-motion", zigbee_motion_driver)

0 commit comments

Comments
 (0)