Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,12 @@ function CameraAttributeHandlers.camera_av_stream_management_attribute_list_hand
attribute_ids = attribute_ids,
}
device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true})
camera_cfg.match_profile(device, status_light_enabled_present, status_light_brightness_present)
camera_cfg.update_status_light_attribute_presence(device, status_light_enabled_present, status_light_brightness_present)
camera_cfg.reconcile_profile_and_capabilities(device)
end

function CameraAttributeHandlers.camera_feature_map_handler(driver, device, ib, response)
camera_cfg.reconcile_profile_and_capabilities(device)
end

return CameraAttributeHandlers
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,152 @@ local switch_utils = require "switch_utils.utils"

local CameraDeviceConfiguration = {}

local managed_capability_map = {
{ key = "webrtc", capability = capabilities.webrtc },
{ key = "ptz", capability = capabilities.mechanicalPanTiltZoom },
{ key = "zone_management", capability = capabilities.zoneManagement },
{ key = "local_media_storage", capability = capabilities.localMediaStorage },
{ key = "audio_recording", capability = capabilities.audioRecording },
{ key = "video_stream_settings", capability = capabilities.videoStreamSettings },
{ key = "camera_privacy_mode", capability = capabilities.cameraPrivacyMode },
}

local function get_status_light_presence(device)
return device:get_field(camera_fields.STATUS_LIGHT_ENABLED_PRESENT),
device:get_field(camera_fields.STATUS_LIGHT_BRIGHTNESS_PRESENT)
end

local function set_status_light_presence(device, status_light_enabled_present, status_light_brightness_present)
device:set_field(camera_fields.STATUS_LIGHT_ENABLED_PRESENT, status_light_enabled_present == true, { persist = true })
device:set_field(camera_fields.STATUS_LIGHT_BRIGHTNESS_PRESENT, status_light_brightness_present == true, { persist = true })
end

local function build_webrtc_supported_features()
return {
bundle = true,
order = "audio/video",
audio = "sendrecv",
video = "recvonly",
turnSource = "player",
supportTrickleICE = true
}
end

local function build_ptz_supported_attributes(device)
local supported_attributes = {}
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then
table.insert(supported_attributes, "pan")
table.insert(supported_attributes, "panRange")
end
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then
table.insert(supported_attributes, "tilt")
table.insert(supported_attributes, "tiltRange")
end
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then
table.insert(supported_attributes, "zoom")
table.insert(supported_attributes, "zoomRange")
end
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPRESETS) then
table.insert(supported_attributes, "presets")
table.insert(supported_attributes, "maxPresets")
end
return supported_attributes
end

local function build_zone_management_supported_features(device)
local supported_features = { "triggerAugmentation" }
if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then
table.insert(supported_features, "perZoneSensitivity")
end
return supported_features
end

local function build_local_media_storage_supported_attributes(device)
local supported_attributes = {}
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then
table.insert(supported_attributes, "localVideoRecording")
end
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then
table.insert(supported_attributes, "localSnapshotRecording")
end
return supported_attributes
end

local function build_video_stream_settings_supported_features(device)
local supported_features = {}
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then
table.insert(supported_features, "liveStreaming")
table.insert(supported_features, "clipRecording")
table.insert(supported_features, "perStreamViewports")
end
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then
table.insert(supported_features, "watermark")
end
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then
table.insert(supported_features, "onScreenDisplay")
end
return supported_features
end

local function build_camera_privacy_supported_attributes()
return { "softRecordingPrivacyMode", "softLivestreamPrivacyMode" }
end

local function build_camera_privacy_supported_commands()
return { "setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode" }
end

local function capabilities_needing_reinit(device)
local main = camera_fields.profile_components.main

local capabilities_to_reinit = {}

local function state_differs(capability, attribute_name, expected)
local current = device:get_latest_state(main, capability.ID, attribute_name)
return not switch_utils.deep_equals(current, expected, { ignore_functions = true })
end

if device:supports_capability(capabilities.webrtc) and
state_differs(capabilities.webrtc, capabilities.webrtc.supportedFeatures.NAME, build_webrtc_supported_features()) then
capabilities_to_reinit.webrtc = true
end

if device:supports_capability(capabilities.mechanicalPanTiltZoom) and
state_differs(capabilities.mechanicalPanTiltZoom, capabilities.mechanicalPanTiltZoom.supportedAttributes.NAME, build_ptz_supported_attributes(device)) then
capabilities_to_reinit.ptz = true
end

if device:supports_capability(capabilities.zoneManagement) and
state_differs(capabilities.zoneManagement, capabilities.zoneManagement.supportedFeatures.NAME, build_zone_management_supported_features(device)) then
capabilities_to_reinit.zone_management = true
end

if device:supports_capability(capabilities.localMediaStorage) and
state_differs(capabilities.localMediaStorage, capabilities.localMediaStorage.supportedAttributes.NAME, build_local_media_storage_supported_attributes(device)) then
capabilities_to_reinit.local_media_storage = true
end

if device:supports_capability(capabilities.audioRecording) then
local audio_enabled_state = device:get_latest_state(main, capabilities.audioRecording.ID, capabilities.audioRecording.audioRecording.NAME)
if audio_enabled_state == nil then
capabilities_to_reinit.audio_recording = true
end
end

if device:supports_capability(capabilities.videoStreamSettings) and
state_differs(capabilities.videoStreamSettings, capabilities.videoStreamSettings.supportedFeatures.NAME, build_video_stream_settings_supported_features(device)) then
capabilities_to_reinit.video_stream_settings = true
end

if device:supports_capability(capabilities.cameraPrivacyMode) and
(state_differs(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedAttributes.NAME, build_camera_privacy_supported_attributes()) or
state_differs(capabilities.cameraPrivacyMode, capabilities.cameraPrivacyMode.supportedCommands.NAME, build_camera_privacy_supported_commands())) then
capabilities_to_reinit.camera_privacy_mode = true
end

return capabilities_to_reinit
end

function CameraDeviceConfiguration.create_child_devices(driver, device)
local num_floodlight_eps = 0
local parent_child_device = false
Expand Down Expand Up @@ -41,7 +187,9 @@ function CameraDeviceConfiguration.create_child_devices(driver, device)
end
end

function CameraDeviceConfiguration.match_profile(device, status_light_enabled_present, status_light_brightness_present)
function CameraDeviceConfiguration.match_profile(device)
local status_light_enabled_present, status_light_brightness_present = get_status_light_presence(device)
local profile_update_requested = false
local optional_supported_component_capabilities = {}
local main_component_capabilities = {}
local status_led_component_capabilities = {}
Expand Down Expand Up @@ -145,78 +293,42 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr
end

if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then
profile_update_requested = true
device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities})
if #doorbell_endpoints > 0 then
CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1])
button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}))
end
end

return profile_update_requested
end

local function init_webrtc(device)
if device:supports_capability(capabilities.webrtc) then
-- TODO: Check for individual audio/video and talkback features
local transport_provider_ep_ids = device:get_endpoints(clusters.WebRTCTransportProvider.ID)
device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures({
value = {
bundle = true,
order = "audio/video",
audio = "sendrecv",
video = "recvonly",
turnSource = "player",
supportTrickleICE = true
}
}))
device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures(build_webrtc_supported_features()))
end
end

local function init_ptz(device)
if device:supports_capability(capabilities.mechanicalPanTiltZoom) then
local supported_attributes = {}
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then
table.insert(supported_attributes, "pan")
table.insert(supported_attributes, "panRange")
end
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then
table.insert(supported_attributes, "tilt")
table.insert(supported_attributes, "tiltRange")
end
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then
table.insert(supported_attributes, "zoom")
table.insert(supported_attributes, "zoomRange")
end
if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPRESETS) then
table.insert(supported_attributes, "presets")
table.insert(supported_attributes, "maxPresets")
end
local av_settings_ep_ids = device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID)
device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(supported_attributes))
device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(build_ptz_supported_attributes(device)))
end
end

local function init_zone_management(device)
if device:supports_capability(capabilities.zoneManagement) then
local supported_features = {}
table.insert(supported_features, "triggerAugmentation")
if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then
table.insert(supported_features, "perZoneSensitivity")
end
local zone_management_ep_ids = device:get_endpoints(clusters.ZoneManagement.ID)
device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(supported_features))
device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(build_zone_management_supported_features(device)))
end
end

local function init_local_media_storage(device)
if device:supports_capability(capabilities.localMediaStorage) then
local supported_attributes = {}
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then
table.insert(supported_attributes, "localVideoRecording")
end
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then
table.insert(supported_attributes, "localSnapshotRecording")
end
local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID)
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(supported_attributes))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(build_local_media_storage_supported_attributes(device)))
end
end

Expand All @@ -235,33 +347,16 @@ end

local function init_video_stream_settings(device)
if device:supports_capability(capabilities.videoStreamSettings) then
local supported_features = {}
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then
table.insert(supported_features, "liveStreaming")
table.insert(supported_features, "clipRecording")
table.insert(supported_features, "perStreamViewports")
end
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then
table.insert(supported_features, "watermark")
end
if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then
table.insert(supported_features, "onScreenDisplay")
end
local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID)
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(supported_features))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(build_video_stream_settings_supported_features(device)))
end
end

local function init_camera_privacy_mode(device)
if device:supports_capability(capabilities.cameraPrivacyMode) then
local supported_attributes, supported_commands = {}, {}
table.insert(supported_attributes, "softRecordingPrivacyMode")
table.insert(supported_attributes, "softLivestreamPrivacyMode")
table.insert(supported_commands, "setSoftRecordingPrivacyMode")
table.insert(supported_commands, "setSoftLivestreamPrivacyMode")
local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID)
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(supported_attributes))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(supported_commands))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(build_camera_privacy_supported_attributes()))
device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(build_camera_privacy_supported_commands()))
end
end

Expand All @@ -275,6 +370,89 @@ function CameraDeviceConfiguration.initialize_camera_capabilities(device)
init_camera_privacy_mode(device)
end

function CameraDeviceConfiguration.initialize_camera_capabilities_and_subscriptions(device)
CameraDeviceConfiguration.initialize_camera_capabilities(device)
device:subscribe()
if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then
button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}))
end
end

local function initialize_selected_camera_capabilities(device, capabilities_to_reinit)
local reinit_targets = capabilities_to_reinit or {}

if reinit_targets.webrtc then
init_webrtc(device)
end
if reinit_targets.ptz then
init_ptz(device)
end
if reinit_targets.zone_management then
init_zone_management(device)
end
if reinit_targets.local_media_storage then
init_local_media_storage(device)
end
if reinit_targets.audio_recording then
init_audio_recording(device)
end
if reinit_targets.video_stream_settings then
init_video_stream_settings(device)
end
if reinit_targets.camera_privacy_mode then
init_camera_privacy_mode(device)
end
end

local function profile_capability_set(profile)
local capability_set = {}
for _, component in pairs((profile or {}).components or {}) do
for _, capability in ipairs(component.capabilities or {}) do
if capability.id ~= nil then
capability_set[capability.id] = true
end
end
end
return capability_set
end

local function changed_capabilities_from_profiles(old_profile, new_profile)
local flags = {}
local old_set = profile_capability_set(old_profile)
local new_set = profile_capability_set(new_profile)

for _, managed in ipairs(managed_capability_map) do
local id = managed.capability.ID
if old_set[id] ~= new_set[id] and new_set[id] == true then
flags[managed.key] = true
end
end

return flags
end

function CameraDeviceConfiguration.reconcile_profile_and_capabilities(device)
local profile_update_requested = CameraDeviceConfiguration.match_profile(device)
if not profile_update_requested then
local capabilities_to_reinit = capabilities_needing_reinit(device)
initialize_selected_camera_capabilities(device, capabilities_to_reinit)
end
return profile_update_requested
end

function CameraDeviceConfiguration.update_status_light_attribute_presence(device, status_light_enabled_present, status_light_brightness_present)
set_status_light_presence(device, status_light_enabled_present, status_light_brightness_present)
end

function CameraDeviceConfiguration.reinitialize_changed_camera_capabilities_and_subscriptions(device, old_profile, new_profile)
local changed_capabilities = changed_capabilities_from_profiles(old_profile, new_profile)
initialize_selected_camera_capabilities(device, changed_capabilities)
device:subscribe()
if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then
button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}))
end
end

function CameraDeviceConfiguration.update_doorbell_component_map(device, ep)
local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}
component_map.doorbell = ep
Expand Down
Loading
Loading