-
Notifications
You must be signed in to change notification settings - Fork 523
Add support for Irrigation System device type #2684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: irrigation-system | ||
| components: | ||
| - id: main | ||
| capabilities: | ||
| - id: valve | ||
| version: 1 | ||
| - id: level | ||
| version: 1 | ||
| config: | ||
| values: | ||
| - key: "level.value" | ||
| range: [0, 100] | ||
| optional: true | ||
| - id: flowSensor | ||
| version: 1 | ||
| optional: true | ||
| - id: operationalState | ||
| version: 1 | ||
| optional: true | ||
| - id: firmwareUpdate | ||
| version: 1 | ||
| - id: refresh | ||
| version: 1 | ||
| categories: | ||
| - name: IrrigationSystem | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -538,4 +538,74 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response) | |
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) | ||
| end | ||
|
|
||
|
|
||
| -- [[ OPERATIONAL STATE CLUSTER ATTRIBUTES ]] -- | ||
|
|
||
| function AttributeHandlers.operational_state_accepted_command_list_attr_handler(driver, device, ib, response) | ||
| local accepted_command_list = {} | ||
| for _, accepted_command in ipairs(ib.data.elements) do | ||
| local accepted_command_id = accepted_command.value | ||
| if fields.operational_state_command_map[accepted_command_id] ~= nil then | ||
| table.insert(accepted_command_list, fields.operational_state_command_map[accepted_command_id]) | ||
| end | ||
| end | ||
| local event = capabilities.operationalState.supportedCommands(accepted_command_list, {visibility = {displayed = false}}) | ||
| device:emit_event_for_endpoint(ib.endpoint_id, event) | ||
| end | ||
|
|
||
| function AttributeHandlers.operational_state_attr_handler(driver, device, ib, response) | ||
| if ib.data.value == clusters.OperationalState.types.OperationalStateEnum.STOPPED then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.stopped()) | ||
| elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.RUNNING then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.running()) | ||
| elseif ib.data.value == clusters.OperationalState.types.OperationalStateEnum.PAUSED then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.paused()) | ||
| end | ||
| end | ||
|
|
||
| function AttributeHandlers.operational_error_attr_handler(driver, device, ib, response) | ||
| if version.api < 10 then | ||
| clusters.OperationalState.types.ErrorStateStruct:augment_type(ib.data) | ||
| end | ||
| local operationalError = ib.data.elements.error_state_id.value | ||
| if operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_START_OR_RESUME then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToStartOrResume()) | ||
| elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.UNABLE_TO_COMPLETE_OPERATION then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.unableToCompleteOperation()) | ||
| elseif operationalError == clusters.OperationalState.types.ErrorStateEnum.COMMAND_INVALID_IN_STATE then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.operationalState.operationalState.commandInvalidInCurrentState()) | ||
| end | ||
| end | ||
|
|
||
| function AttributeHandlers.flow_attr_handler(driver, device, ib, response) | ||
| local measured_value = ib.data.value | ||
| if measured_value ~= nil then | ||
| local flow = measured_value / 10.0 | ||
| local unit = "m^3/h" | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) | ||
| end | ||
| end | ||
|
|
||
| function AttributeHandlers.flow_attr_handler_factory(minOrMax) | ||
| return function(driver, device, ib, response) | ||
| if ib.data.value == nil then | ||
| return | ||
| end | ||
| local flow_bound = ib.data.value / 10.0 | ||
| local unit = "m^3/h" | ||
| set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) | ||
| local min = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think these fields are actually mapped to anything as they are now, since these are defined in |
||
| local max = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id) | ||
| if min ~= nil and max ~= nil then | ||
| if min < max then | ||
| device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) | ||
| set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id, nil) | ||
| set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id, nil) | ||
| else | ||
| device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max)) | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| return AttributeHandlers | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ end | |
| local DeviceConfiguration = {} | ||
| local SwitchDeviceConfiguration = {} | ||
| local ButtonDeviceConfiguration = {} | ||
| local ValveDeviceConfiguration = {} | ||
|
|
||
| function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) | ||
| local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) | ||
|
|
@@ -156,6 +157,48 @@ function ButtonDeviceConfiguration.configure_buttons(device) | |
| end | ||
| end | ||
|
|
||
| function ValveDeviceConfiguration.assign_profile_for_valve_ep(device, valve_ep_id) | ||
| local profile = "water-valve" | ||
|
|
||
| for _, ep in ipairs(device.endpoints) do | ||
| if ep.endpoint_id == valve_ep_id then | ||
|
Comment on lines
+163
to
+164
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can use something like |
||
| if switch_utils.find_cluster_on_ep(ep, clusters.ValveConfigurationAndControl.ID) then | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this helper can take the feature map as a third parameter, might simplify this a bit more |
||
| for _, cluster in ipairs(ep.clusters) do | ||
| if cluster.cluster_id == clusters.ValveConfigurationAndControl.ID and | ||
| cluster.feature_map and (cluster.feature_map & clusters.ValveConfigurationAndControl.types.Feature.LEVEL) ~= 0 then | ||
| profile = profile .. "-level" | ||
| break | ||
| end | ||
| end | ||
| end | ||
| break | ||
| end | ||
| end | ||
|
|
||
| return profile | ||
| end | ||
|
|
||
| function ValveDeviceConfiguration.create_child_devices(driver, device, valve_ep_ids, default_endpoint_id) | ||
| for device_num, ep_id in ipairs(valve_ep_ids) do | ||
| local label_and_name = string.format("%s Valve %d", device.label, device_num) | ||
| local child_profile = ValveDeviceConfiguration.assign_profile_for_valve_ep(device, ep_id) | ||
| driver:try_create_device( | ||
| { | ||
| type = "EDGE_CHILD", | ||
| label = label_and_name, | ||
| profile = child_profile, | ||
| parent_device_id = device.id, | ||
| parent_assigned_child_key = string.format("%d", ep_id), | ||
| vendor_provided_label = label_and_name | ||
| } | ||
| ) | ||
| end | ||
|
|
||
| -- Persist so that the find_child function is always set on each driver init. | ||
| device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) | ||
| device:set_find_child(switch_utils.find_child) | ||
| end | ||
|
|
||
|
|
||
| -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- | ||
|
|
||
|
|
@@ -174,7 +217,61 @@ function DeviceConfiguration.match_profile(driver, device) | |
| local default_endpoint_id = switch_utils.find_default_endpoint(device) | ||
| local updated_profile | ||
|
|
||
| if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then | ||
| local is_irrigation_system = false | ||
| for _, ep in ipairs(device.endpoints) do | ||
| for _, dt in ipairs(ep.device_types) do | ||
| if dt.device_type_id == fields.DEVICE_TYPE_ID.IRRIGATION_SYSTEM then | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe use the |
||
| is_irrigation_system = true | ||
| break | ||
| end | ||
| end | ||
| end | ||
|
|
||
| local valve_ep_ids = device:get_endpoints(clusters.ValveConfigurationAndControl.ID) | ||
|
|
||
| if is_irrigation_system then | ||
| updated_profile = "irrigation-system" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'm a little bit unsure why we even have to make an
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, if we're adding flow measurement, the spec defines that all water valves might have flow support optionally... may want to add that as well? This would mean that as far as the profile, the only difference we'd see between irrigation-system and water-valve would be the operational state capability at this time. Since that can be handled modularly,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. However, I see you're using a different category for IrrigationSystem. So I like the idea of having the separate profile. We still may want to add flow to water valve though |
||
| if version.api >= 14 and version.rpc >= 8 then | ||
| local main_component_capabilities = {} | ||
| local optional_supported_component_capabilities = {} | ||
| local MAIN_COMPONENT_IDX, CAPABILITIES_LIST_IDX = 1, 2 | ||
|
|
||
| table.sort(valve_ep_ids) | ||
| local main_valve_ep = switch_utils.get_endpoint_info(device, valve_ep_ids[1]) | ||
| for _, cluster in ipairs(main_valve_ep.clusters) do | ||
| if cluster.cluster_id == clusters.ValveConfigurationAndControl.ID and | ||
| cluster.feature_map and (cluster.feature_map & clusters.ValveConfigurationAndControl.types.Feature.LEVEL) ~= 0 then | ||
| table.insert(main_component_capabilities, capabilities.level.ID) | ||
| elseif cluster.cluster_id == clusters.OperationalState.ID then | ||
| table.insert(main_component_capabilities, capabilities.operationalState.ID) | ||
| elseif cluster.cluster_id == clusters.FlowMeasurement.ID then | ||
| table.insert(main_component_capabilities, capabilities.flowSensor.ID) | ||
| end | ||
| end | ||
|
|
||
| table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) | ||
|
|
||
| device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) | ||
|
|
||
| -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. | ||
| -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. | ||
| if version.api < 15 or version.rpc < 9 then | ||
| -- add mandatory capabilities for subscription | ||
| local total_supported_capabilities = optional_supported_component_capabilities | ||
| table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) | ||
| table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) | ||
|
|
||
| device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're going to add support for pre-api 15, we'll also have to extend in init and info_changed some logic like this: I'm not totally convinced this is a worthwhile path to take though... but that may just be me |
||
| end | ||
| else | ||
| device:try_update_metadata({profile = profile_name}) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this api should have 2 calls in 2 different places for I haven't looked at it super closely, but most of this could/should just be handled within the |
||
| end | ||
|
|
||
| if #valve_ep_ids > 1 then | ||
| table.remove(valve_ep_ids, 1) -- the first valve ep is accounted for in the main irrigation system profile | ||
| ValveDeviceConfiguration.create_child_devices(driver, device, valve_ep_ids, default_endpoint_id) | ||
|
Comment on lines
+270
to
+272
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might move this logic into the create_child_devices function itself- see the create_child_devices for onOff eps to see what I'm suggesting (since we do the same behavior there as well)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could also plug this logic within the second check, rather than doing an if-else. Like: |
||
| end | ||
| elseif #valve_ep_ids > 0 then | ||
| updated_profile = "water-valve" | ||
| if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, | ||
| {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then | ||
|
|
@@ -221,5 +318,6 @@ end | |
| return { | ||
| DeviceCfg = DeviceConfiguration, | ||
| SwitchCfg = SwitchDeviceConfiguration, | ||
| ButtonCfg = ButtonDeviceConfiguration | ||
| ButtonCfg = ButtonDeviceConfiguration, | ||
| ValveCfg = ValveDeviceConfiguration | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that in 66bf122 I reimplemented
levelas an optional capabilty in this profile (as well as added flowSensor + operationalState). I think this makes sense but let me know if you agree with this approach or not