@@ -50,12 +50,9 @@ local SWITCH_INITIALIZED = "__switch_intialized"
50
50
-- in the device table for devices that joined prior to this transition, and it
51
51
-- will not be set for new devices.
52
52
local COMPONENT_TO_ENDPOINT_MAP = " __component_to_endpoint_map"
53
- -- COMPONENT_TO_ENDPOINT_MAP_BUTTON is for devices with button endpoints, to
54
- -- preserve the MCD functionality for button devices from the matter-button
55
- -- driver after it was merged into the matter-switch driver. Note that devices
56
- -- containing both button endpoints and switch endpoints will use this field
57
- -- rather than COMPONENT_TO_ENDPOINT_MAP.
58
- local COMPONENT_TO_ENDPOINT_MAP_BUTTON = " __component_to_endpoint_map_button"
53
+ -- COMPONENT_TO_ENDPOINT_MAP_NEW_DEVICES is for new devices that can be supported
54
+ -- by a MCD configuration.
55
+ local COMPONENT_TO_ENDPOINT_MAP_NEW_DEVICES = " __component_to_endpoint_map_button"
59
56
local ENERGY_MANAGEMENT_ENDPOINT = " __energy_management_endpoint"
60
57
local IS_PARENT_CHILD_DEVICE = " __is_parent_child_device"
61
58
local COLOR_TEMP_BOUND_RECEIVED_KELVIN = " __colorTemp_bound_received_kelvin"
@@ -291,7 +288,7 @@ local HELD_THRESHOLD = 1
291
288
-- this is the number of buttons for which we have a static profile already made
292
289
local STATIC_BUTTON_PROFILE_SUPPORTED = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 }
293
290
294
- local BUTTON_DEVICE_PROFILED = " __button_device_profiled "
291
+ local DEVICE_PROFILED = " __device_profiled "
295
292
296
293
-- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a
297
294
-- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because
@@ -456,15 +453,15 @@ local function find_default_endpoint(device)
456
453
end
457
454
458
455
local function component_to_endpoint (device , component )
459
- local map = device :get_field (COMPONENT_TO_ENDPOINT_MAP_BUTTON ) or device :get_field (COMPONENT_TO_ENDPOINT_MAP ) or {}
456
+ local map = device :get_field (COMPONENT_TO_ENDPOINT_MAP_NEW_DEVICES ) or device :get_field (COMPONENT_TO_ENDPOINT_MAP ) or {}
460
457
if map [component ] then
461
458
return map [component ]
462
459
end
463
460
return find_default_endpoint (device )
464
461
end
465
462
466
463
local function endpoint_to_component (device , ep )
467
- local map = device :get_field (COMPONENT_TO_ENDPOINT_MAP_BUTTON ) or device :get_field (COMPONENT_TO_ENDPOINT_MAP ) or {}
464
+ local map = device :get_field (COMPONENT_TO_ENDPOINT_MAP_NEW_DEVICES ) or device :get_field (COMPONENT_TO_ENDPOINT_MAP ) or {}
468
465
for component , endpoint in pairs (map ) do
469
466
if endpoint == ep then
470
467
return component
@@ -513,7 +510,7 @@ local function assign_child_profile(device, child_ep)
513
510
end
514
511
515
512
local function do_configure (driver , device )
516
- if device :get_field (BUTTON_DEVICE_PROFILED ) then
513
+ if device :get_field (DEVICE_PROFILED ) then
517
514
return
518
515
end
519
516
local level_eps = embedded_cluster_utils .get_endpoints (device , clusters .LevelControl .ID )
@@ -586,45 +583,66 @@ local function find_child(parent, ep_id)
586
583
return parent :get_child_by_parent_assigned_key (string.format (" %d" , ep_id ))
587
584
end
588
585
589
- local function try_build_button_component_map (device , main_endpoint , button_eps )
590
- -- create component mapping on the main profile button endpoints
591
- if STATIC_BUTTON_PROFILE_SUPPORTED [# button_eps ] then
592
- local component_map = {}
593
- component_map [" main" ] = main_endpoint
594
- for component_num , ep in ipairs (button_eps ) do
595
- if ep ~= main_endpoint then
596
- local button_component = " button" .. component_num
597
- component_map [button_component ] = ep
586
+ local function build_component_map (device , main_endpoint , endpoints )
587
+ local button_eps = device :get_endpoints (clusters .Switch .ID , {feature_bitmap = clusters .Switch .types .SwitchFeature .MOMENTARY_SWITCH })
588
+ local fan_eps = device :get_endpoints (clusters .FanControl .ID )
589
+ local component_name
590
+
591
+ if # button_eps > 0 and STATIC_BUTTON_PROFILE_SUPPORTED [# button_eps ] then
592
+ component_name = " button"
593
+ elseif # fan_eps > 0 then
594
+ component_name = " fan"
595
+ else
596
+ device .log .warn_with ({hub_logs = true }, " Device is not supported by a multicomponent configuration" )
597
+ return
598
+ end
599
+
600
+ local component_map = {}
601
+ component_map [" main" ] = main_endpoint
602
+ for component_num , ep in ipairs (endpoints ) do
603
+ if ep ~= main_endpoint then
604
+ local component = component_name
605
+ if # endpoints > 1 then
606
+ component = component .. component_num
598
607
end
608
+ component_map [component ] = ep
599
609
end
600
- device :set_field (COMPONENT_TO_ENDPOINT_MAP_BUTTON , component_map , {persist = true })
601
610
end
611
+ device :set_field (COMPONENT_TO_ENDPOINT_MAP_NEW_DEVICES , component_map , {persist = true })
602
612
end
603
613
604
- local function build_button_profile (device , main_endpoint , num_button_eps )
614
+ local function build_mcd_profile (device , main_endpoint )
615
+ local button_eps = device :get_endpoints (clusters .Switch .ID , {feature_bitmap = clusters .Switch .types .SwitchFeature .MOMENTARY_SWITCH })
616
+ local fan_eps = device :get_endpoints (clusters .FanControl .ID )
605
617
local profile_name
606
618
local battery_supported
619
+
607
620
if device_type_supports_button_switch_combination (device , main_endpoint ) then
608
- profile_name = " light-level-" .. num_button_eps .. " -button"
609
- else
610
- profile_name = num_button_eps .. " -button"
621
+ profile_name = " light-level-" .. # button_eps .. " -button"
622
+ elseif # button_eps > 0 and STATIC_BUTTON_PROFILE_SUPPORTED [ # button_eps ] then
623
+ profile_name = # button_eps .. " -button"
611
624
battery_supported = # device :get_endpoints (clusters .PowerSource .ID , {feature_bitmap = clusters .PowerSource .types .PowerSourceFeature .BATTERY }) > 0
612
625
if device .manufacturer_info .vendor_id == HUE_MANUFACTURER_ID then battery_supported = false end -- no battery support in Hue case
613
626
if battery_supported then
614
627
local attribute_list_read = im .InteractionRequest (im .InteractionRequest .RequestType .READ , {})
615
628
attribute_list_read :merge (clusters .PowerSource .attributes .AttributeList :read ())
616
629
device :send (attribute_list_read )
617
630
end
631
+ elseif # fan_eps > 0 then
632
+ profile_name = " light-color-level-fan"
633
+ else
634
+ device .log .warn_with ({hub_logs = true }, " Device is not supported by a multicomponent profile" )
635
+ return
618
636
end
619
637
620
638
if not battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler
621
639
profile_name = string.gsub (profile_name , " 1%-" , " " ) -- remove the "1-" in a device with 1 button ep
622
640
device :try_update_metadata ({profile = profile_name })
623
641
end
624
- device :set_field (BUTTON_DEVICE_PROFILED , true )
642
+ device :set_field (DEVICE_PROFILED , true )
625
643
end
626
644
627
- local function try_build_child_switch_profiles (driver , device , switch_eps , main_endpoint )
645
+ local function build_child_switch_profiles (driver , device , switch_eps , main_endpoint )
628
646
local num_switch_server_eps = 0
629
647
local parent_child_device = false
630
648
for _ , ep in ipairs (switch_eps ) do
@@ -658,58 +676,63 @@ local function try_build_child_switch_profiles(driver, device, switch_eps, main_
658
676
device :set_field (IS_PARENT_CHILD_DEVICE , true , {persist = true })
659
677
end
660
678
661
- device :set_field (SWITCH_INITIALIZED , true , {persist = true })
662
-
663
679
-- this is needed in initialize_buttons_and_switches
664
680
return num_switch_server_eps
665
681
end
666
682
667
- local function handle_light_switch_with_onOff_server_clusters (device , main_endpoint , num_switch_server_eps )
668
- local cluster_id = 0
669
- for _ , ep in ipairs (device .endpoints ) do
670
- -- main_endpoint only supports server cluster by definition of get_endpoints()
671
- if main_endpoint == ep .endpoint_id then
672
- for _ , dt in ipairs (ep .device_types ) do
673
- -- no device type that is not in the switch subset should be considered.
674
- if (ON_OFF_SWITCH_ID <= dt .device_type_id and dt .device_type_id <= ON_OFF_COLOR_DIMMER_SWITCH_ID ) then
675
- cluster_id = math.max (cluster_id , dt .device_type_id )
676
- end
683
+ local function handle_light_switch_with_onOff_server_clusters (device , main_endpoint )
684
+ local cluster_id = 0
685
+ for _ , ep in ipairs (device .endpoints ) do
686
+ -- main_endpoint only supports server cluster by definition of get_endpoints()
687
+ if main_endpoint == ep .endpoint_id then
688
+ for _ , dt in ipairs (ep .device_types ) do
689
+ -- no device type that is not in the switch subset should be considered.
690
+ if (ON_OFF_SWITCH_ID <= dt .device_type_id and dt .device_type_id <= ON_OFF_COLOR_DIMMER_SWITCH_ID ) then
691
+ cluster_id = math.max (cluster_id , dt .device_type_id )
677
692
end
678
- break
679
693
end
694
+ break
680
695
end
696
+ end
681
697
682
- if device_type_profile_map [cluster_id ] then
683
- device :try_update_metadata ({profile = device_type_profile_map [cluster_id ]})
684
- end
698
+ if device_type_profile_map [cluster_id ] then
699
+ device :try_update_metadata ({profile = device_type_profile_map [cluster_id ]})
700
+ end
685
701
end
686
702
687
703
local function initialize_buttons_and_switches (driver , device , main_endpoint )
688
704
local switch_eps = device :get_endpoints (clusters .OnOff .ID )
689
705
local button_eps = device :get_endpoints (clusters .Switch .ID , {feature_bitmap = clusters .Switch .types .SwitchFeature .MOMENTARY_SWITCH })
706
+ local fan_eps = device :get_endpoints (clusters .FanControl .ID )
690
707
table.sort (switch_eps )
691
708
table.sort (button_eps )
709
+ table.sort (fan_eps )
692
710
693
- -- All button endpoints found will be added as additional components in the profile containing the main_endpoint.
694
- -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP_BUTTON field
695
- try_build_button_component_map (device , main_endpoint , button_eps )
696
-
697
- -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled
698
- -- while building switch child profiles
699
- local num_switch_server_eps = try_build_child_switch_profiles (driver , device , switch_eps , main_endpoint )
700
-
711
+ -- Button/fan endpoints will be added as additional components in the profile containing the main_endpoint.
712
+ -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP_NEW_DEVICES field
701
713
if # button_eps > 0 then
702
- build_button_profile (device , main_endpoint , # button_eps )
714
+ build_mcd_profile (device , main_endpoint )
715
+ build_component_map (device , main_endpoint , button_eps )
703
716
configure_buttons (device )
704
- return
717
+ elseif # fan_eps > 0 then
718
+ build_mcd_profile (device , main_endpoint )
719
+ build_component_map (device , main_endpoint , fan_eps )
705
720
end
706
721
707
- -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings.
708
- -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'.
709
- -- Note: since their device type isn't supported, these devices join as a matter-thing.
710
- if num_switch_server_eps > 0 and detect_matter_thing (device ) then
711
- handle_light_switch_with_onOff_server_clusters (device , main_endpoint , num_switch_server_eps )
722
+ if # switch_eps > 0 then
723
+ -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled
724
+ -- while building switch child profiles
725
+ local num_switch_server_eps = build_child_switch_profiles (driver , device , switch_eps , main_endpoint )
726
+
727
+ -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings.
728
+ -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'.
729
+ -- Note: since their device type isn't supported, these devices join as a matter-thing.
730
+ if num_switch_server_eps > 0 and detect_matter_thing (device ) then
731
+ handle_light_switch_with_onOff_server_clusters (device , main_endpoint )
732
+ end
712
733
end
734
+
735
+ device :set_field (SWITCH_INITIALIZED , true , {persist = true })
713
736
end
714
737
715
738
local function detect_bridge (device )
@@ -883,6 +906,29 @@ local function handle_set_level(driver, device, cmd)
883
906
end
884
907
end
885
908
909
+ local function set_fan_mode (driver , device , cmd )
910
+ local fan_mode_id
911
+ if cmd .args .fanMode == capabilities .fanMode .fanMode .low .NAME then
912
+ fan_mode_id = clusters .FanControl .attributes .FanMode .LOW
913
+ elseif cmd .args .fanMode == capabilities .fanMode .fanMode .medium .NAME then
914
+ fan_mode_id = clusters .FanControl .attributes .FanMode .MEDIUM
915
+ elseif cmd .args .fanMode == capabilities .fanMode .fanMode .high .NAME then
916
+ fan_mode_id = clusters .FanControl .attributes .FanMode .HIGH
917
+ elseif cmd .args .fanMode == capabilities .fanMode .fanMode .auto .NAME then
918
+ fan_mode_id = clusters .FanControl .attributes .FanMode .AUTO
919
+ else
920
+ fan_mode_id = clusters .FanControl .attributes .FanMode .OFF
921
+ end
922
+ if fan_mode_id then
923
+ device :send (clusters .FanControl .attributes .FanMode :write (device , device :component_to_endpoint (cmd .component ), fan_mode_id ))
924
+ end
925
+ end
926
+
927
+ local function set_fan_speed_percent (driver , device , cmd )
928
+ local speed = math.floor (cmd .args .percent )
929
+ device :send (clusters .FanControl .attributes .PercentSetting :write (device , device :component_to_endpoint (cmd .component ), speed ))
930
+ end
931
+
886
932
local function handle_refresh (driver , device , cmd )
887
933
-- Note: no endpoint specified indicates a wildcard endpoint
888
934
local req = clusters .OnOff .attributes .OnOff :read (device )
@@ -1340,6 +1386,73 @@ local function humidity_attr_handler(driver, device, ib, response)
1340
1386
end
1341
1387
end
1342
1388
1389
+ local function fan_mode_handler (driver , device , ib , response )
1390
+ if ib .data .value == clusters .FanControl .attributes .FanMode .OFF then
1391
+ device :emit_event_for_endpoint (ib .endpoint_id , capabilities .fanMode .fanMode (" off" ))
1392
+ elseif ib .data .value == clusters .FanControl .attributes .FanMode .LOW then
1393
+ device :emit_event_for_endpoint (ib .endpoint_id , capabilities .fanMode .fanMode (" low" ))
1394
+ elseif ib .data .value == clusters .FanControl .attributes .FanMode .MEDIUM then
1395
+ device :emit_event_for_endpoint (ib .endpoint_id , capabilities .fanMode .fanMode (" medium" ))
1396
+ elseif ib .data .value == clusters .FanControl .attributes .FanMode .HIGH then
1397
+ device :emit_event_for_endpoint (ib .endpoint_id , capabilities .fanMode .fanMode (" high" ))
1398
+ else
1399
+ device :emit_event_for_endpoint (ib .endpoint_id , capabilities .fanMode .fanMode (" auto" ))
1400
+ end
1401
+ end
1402
+
1403
+ local function fan_mode_sequence_handler (driver , device , ib , response )
1404
+ local supportedFanModes
1405
+ if ib .data .value == clusters .FanControl .attributes .FanModeSequence .OFF_LOW_MED_HIGH then
1406
+ supportedFanModes = {
1407
+ capabilities .fanMode .fanMode .off .NAME ,
1408
+ capabilities .fanMode .fanMode .low .NAME ,
1409
+ capabilities .fanMode .fanMode .medium .NAME ,
1410
+ capabilities .fanMode .fanMode .high .NAME
1411
+ }
1412
+ elseif ib .data .value == clusters .FanControl .attributes .FanModeSequence .OFF_LOW_HIGH then
1413
+ supportedFanModes = {
1414
+ capabilities .fanMode .fanMode .off .NAME ,
1415
+ capabilities .fanMode .fanMode .low .NAME ,
1416
+ capabilities .fanMode .fanMode .high .NAME
1417
+ }
1418
+ elseif ib .data .value == clusters .FanControl .attributes .FanModeSequence .OFF_LOW_MED_HIGH_AUTO then
1419
+ supportedFanModes = {
1420
+ capabilities .fanMode .fanMode .off .NAME ,
1421
+ capabilities .fanMode .fanMode .low .NAME ,
1422
+ capabilities .fanMode .fanMode .medium .NAME ,
1423
+ capabilities .fanMode .fanMode .high .NAME ,
1424
+ capabilities .fanMode .fanMode .auto .NAME
1425
+ }
1426
+ elseif ib .data .value == clusters .FanControl .attributes .FanModeSequence .OFF_LOW_HIGH_AUTO then
1427
+ supportedFanModes = {
1428
+ capabilities .fanMode .fanMode .off .NAME ,
1429
+ capabilities .fanMode .fanMode .low .NAME ,
1430
+ capabilities .fanMode .fanMode .high .NAME ,
1431
+ capabilities .fanMode .fanMode .auto .NAME
1432
+ }
1433
+ elseif ib .data .value == clusters .FanControl .attributes .FanModeSequence .OFF_ON_AUTO then
1434
+ supportedFanModes = {
1435
+ capabilities .fanMode .fanMode .off .NAME ,
1436
+ capabilities .fanMode .fanMode .high .NAME ,
1437
+ capabilities .fanMode .fanMode .auto .NAME
1438
+ }
1439
+ else
1440
+ supportedFanModes = {
1441
+ capabilities .fanMode .fanMode .off .NAME ,
1442
+ capabilities .fanMode .fanMode .high .NAME
1443
+ }
1444
+ end
1445
+ local event = capabilities .fanMode .supportedFanModes (supportedFanModes , {visibility = {displayed = false }})
1446
+ device :emit_event_for_endpoint (ib .endpoint_id , event )
1447
+ end
1448
+
1449
+ local function fan_speed_percent_attr_handler (driver , device , ib , response )
1450
+ if ib .data .value == nil or ib .data .value < 0 or ib .data .value > 100 then
1451
+ return
1452
+ end
1453
+ device :emit_event_for_endpoint (ib .endpoint_id , capabilities .fanSpeedPercent .percent (ib .data .value ))
1454
+ end
1455
+
1343
1456
local matter_driver_template = {
1344
1457
lifecycle_handlers = {
1345
1458
init = device_init ,
@@ -1401,6 +1514,11 @@ local matter_driver_template = {
1401
1514
[clusters .TemperatureMeasurement .attributes .MeasuredValue .ID ] = temperature_attr_handler ,
1402
1515
[clusters .TemperatureMeasurement .attributes .MinMeasuredValue .ID ] = temp_attr_handler_factory (TEMP_MIN ),
1403
1516
[clusters .TemperatureMeasurement .attributes .MaxMeasuredValue .ID ] = temp_attr_handler_factory (TEMP_MAX ),
1517
+ },
1518
+ [clusters .FanControl .ID ] = {
1519
+ [clusters .FanControl .attributes .FanModeSequence .ID ] = fan_mode_sequence_handler ,
1520
+ [clusters .FanControl .attributes .FanMode .ID ] = fan_mode_handler ,
1521
+ [clusters .FanControl .attributes .PercentCurrent .ID ] = fan_speed_percent_attr_handler
1404
1522
}
1405
1523
},
1406
1524
event = {
@@ -1466,6 +1584,13 @@ local matter_driver_template = {
1466
1584
clusters .TemperatureMeasurement .attributes .MeasuredValue ,
1467
1585
clusters .TemperatureMeasurement .attributes .MinMeasuredValue ,
1468
1586
clusters .TemperatureMeasurement .attributes .MaxMeasuredValue
1587
+ },
1588
+ [capabilities .fanMode .ID ] = {
1589
+ clusters .FanControl .attributes .FanModeSequence ,
1590
+ clusters .FanControl .attributes .FanMode
1591
+ },
1592
+ [capabilities .fanSpeedPercent .ID ] = {
1593
+ clusters .FanControl .attributes .PercentCurrent
1469
1594
}
1470
1595
},
1471
1596
subscribed_events = {
@@ -1501,6 +1626,12 @@ local matter_driver_template = {
1501
1626
},
1502
1627
[capabilities .level .ID ] = {
1503
1628
[capabilities .level .commands .setLevel .NAME ] = handle_set_level
1629
+ },
1630
+ [capabilities .fanMode .ID ] = {
1631
+ [capabilities .fanMode .commands .setFanMode .NAME ] = set_fan_mode
1632
+ },
1633
+ [capabilities .fanSpeedPercent .ID ] = {
1634
+ [capabilities .fanSpeedPercent .commands .setPercent .NAME ] = set_fan_speed_percent ,
1504
1635
}
1505
1636
},
1506
1637
supported_capabilities = {
@@ -1519,7 +1650,9 @@ local matter_driver_template = {
1519
1650
capabilities .battery ,
1520
1651
capabilities .batteryLevel ,
1521
1652
capabilities .temperatureMeasurement ,
1522
- capabilities .relativeHumidityMeasurement
1653
+ capabilities .relativeHumidityMeasurement ,
1654
+ capabilities .fanMode ,
1655
+ capabilities .fanSpeedPercent
1523
1656
},
1524
1657
sub_drivers = {
1525
1658
require (" eve-energy" ),
0 commit comments