@@ -19,6 +19,10 @@ const MOTION_SERVICE_RID_POINTER: &str = "/motion/motion_service/rid";
1919pub const DARK_THRESHOLD_POINTER : & str =
2020 "/light_level/daylight/daylight_sensitivity/settings/dark_threshold" ;
2121
22+ /// Where a motion automation names the light reading it compares against.
23+ const LIGHT_LEVEL_SERVICE_RID_POINTER : & str =
24+ "/light_level/daylight/daylight_sensitivity/light_level_service/rid" ;
25+
2226/// The `dark_threshold` of an automation that ignores daylight: the top of the
2327/// light level scale, so it is always dark.
2428pub const DAYLIGHT_OFF : u32 = 65534 ;
@@ -30,6 +34,8 @@ pub struct MotionInputs<'a> {
3034 pub grouped : & ' a [ GroupedMotionResource ] ,
3135 pub service_groups : & ' a [ ServiceGroupResource ] ,
3236 pub automations : & ' a [ BehaviorInstanceResource ] ,
37+ /// Both `light_level` and `grouped_light_level` resources.
38+ pub light_levels : & ' a [ LightLevelResource ] ,
3339 /// device ID → name
3440 pub device_names : & ' a HashMap < & ' a str , & ' a str > ,
3541}
@@ -79,7 +85,7 @@ pub fn build_motion_units(inputs: &MotionInputs) -> Vec<MotionUnit> {
7985 }
8086 } ) ) ;
8187
82- let daylight_by_service = daylight_by_service ( inputs. automations ) ;
88+ let daylight_by_service = daylight_by_service ( inputs. automations , inputs . light_levels ) ;
8389 let grouped_by_id: HashMap < & str , & GroupedMotionResource > =
8490 inputs. grouped . iter ( ) . map ( |g| ( g. id . as_str ( ) , g) ) . collect ( ) ;
8591
@@ -160,7 +166,14 @@ pub fn set_dark_threshold(configuration: &mut Value, threshold: Option<u32>) ->
160166}
161167
162168/// motion service ID → the daylight setting of the first automation on it.
163- fn daylight_by_service ( automations : & [ BehaviorInstanceResource ] ) -> HashMap < & str , Daylight > {
169+ fn daylight_by_service < ' a > (
170+ automations : & ' a [ BehaviorInstanceResource ] ,
171+ light_levels : & [ LightLevelResource ] ,
172+ ) -> HashMap < & ' a str , Daylight > {
173+ let level_by_service: HashMap < & str , Option < u32 > > = light_levels
174+ . iter ( )
175+ . map ( |l| ( l. id . as_str ( ) , l. light . current ( ) ) )
176+ . collect ( ) ;
164177 let mut map = HashMap :: new ( ) ;
165178 for automation in automations {
166179 let Some ( rid) = automation
@@ -173,9 +186,17 @@ fn daylight_by_service(automations: &[BehaviorInstanceResource]) -> HashMap<&str
173186 let Some ( threshold) = dark_threshold ( & automation. configuration ) else {
174187 continue ;
175188 } ;
189+ let light_level_service = automation
190+ . configuration
191+ . pointer ( LIGHT_LEVEL_SERVICE_RID_POINTER )
192+ . and_then ( Value :: as_str) ;
176193 map. entry ( rid) . or_insert_with ( || Daylight {
177194 automation_id : automation. id . clone ( ) ,
178195 dark_threshold : threshold,
196+ light_level_service_id : light_level_service. map ( str:: to_owned) ,
197+ light_level : light_level_service
198+ . and_then ( |id| level_by_service. get ( id) . copied ( ) )
199+ . flatten ( ) ,
179200 } ) ;
180201 }
181202 map
@@ -208,14 +229,14 @@ mod tests {
208229 json ! ( { "motion_report" : { "changed" : "2026-10-04T09:00:00Z" , "motion" : motion} } )
209230 }
210231
211- fn automation ( id : & str , service : & str , threshold : Option < u32 > ) -> Value {
232+ fn automation ( id : & str , service : & str , light : & str , threshold : Option < u32 > ) -> Value {
212233 let mut configuration = json ! ( {
213234 "motion" : { "motion_service" : { "rid" : service, "rtype" : "motion" } } ,
214235 "source" : { "rid" : "device-x" , "rtype" : "device" } ,
215236 } ) ;
216237 if let Some ( t) = threshold {
217238 configuration[ "light_level" ] = json ! ( { "daylight" : { "daylight_sensitivity" : {
218- "light_level_service" : { "rid" : "ll" , "rtype" : "light_level" } ,
239+ "light_level_service" : { "rid" : light , "rtype" : "light_level" } ,
219240 "settings" : { "dark_threshold" : t, "offset" : 7000 } ,
220241 } } } ) ;
221242 }
@@ -255,11 +276,17 @@ mod tests {
255276 ] } ,
256277 ] ) ) ;
257278 let automations = parse :: < BehaviorInstanceResource > ( json ! ( [
258- automation( "b-kitchen" , "g-kitchen" , Some ( 14477 ) ) ,
259- automation( "b-hall" , "m-hall" , Some ( DAYLIGHT_OFF ) ) ,
260- automation( "b-hall-2" , "m-hall" , Some ( 100 ) ) ,
279+ automation( "b-kitchen" , "g-kitchen" , "gl-kitchen" , Some ( 14477 ) ) ,
280+ automation( "b-hall" , "m-hall" , "ll-hall" , Some ( DAYLIGHT_OFF ) ) ,
281+ automation( "b-hall-2" , "m-hall" , "ll-hall" , Some ( 100 ) ) ,
261282 { "id" : "b-other" , "configuration" : { "when" : { } } } ,
262283 ] ) ) ;
284+ // The hall sensor has not reported yet.
285+ let light_levels = parse :: < LightLevelResource > ( json ! ( [
286+ { "id" : "gl-kitchen" , "light" : { "light_level_report" :
287+ { "changed" : "2026-10-04T09:00:00Z" , "light_level" : 17723 } } } ,
288+ { "id" : "ll-kitchen" , "light" : { "light_level" : 17000 } } ,
289+ ] ) ) ;
263290 let device_names = HashMap :: from ( [ ( "d-kitchen" , "Keittiön anturi" ) , ( "d-hall" , "Käytävä" ) ] ) ;
264291
265292 build_motion_units ( & MotionInputs {
@@ -269,6 +296,7 @@ mod tests {
269296 grouped : & grouped,
270297 service_groups : & service_groups,
271298 automations : & automations,
299+ light_levels : & light_levels,
272300 device_names : & device_names,
273301 } )
274302 }
@@ -349,9 +377,31 @@ mod tests {
349377 assert_eq ! ( hall_daylight. dark_threshold, None , "65534 ignores daylight" ) ;
350378 }
351379
380+ #[ test]
381+ fn daylight_carries_the_reading_the_automation_compares ( ) {
382+ let units = units ( ) ;
383+ let kitchen = units. iter ( ) . find ( |u| u. id == "sg-kitchen" ) . unwrap ( ) ;
384+ let hall = units. iter ( ) . find ( |u| u. id == "m-hall" ) . unwrap ( ) ;
385+
386+ let kitchen_daylight = kitchen. daylight . as_ref ( ) . unwrap ( ) ;
387+ assert_eq ! (
388+ kitchen_daylight. light_level_service_id. as_deref( ) ,
389+ Some ( "gl-kitchen" ) ,
390+ "the group's reading, not its sensor's"
391+ ) ;
392+ assert_eq ! ( kitchen_daylight. light_level, Some ( 17723 ) ) ;
393+
394+ let hall_daylight = hall. daylight . as_ref ( ) . unwrap ( ) ;
395+ assert_eq ! (
396+ hall_daylight. light_level_service_id. as_deref( ) ,
397+ Some ( "ll-hall" )
398+ ) ;
399+ assert_eq ! ( hall_daylight. light_level, None ) ;
400+ }
401+
352402 #[ test]
353403 fn set_dark_threshold_rewrites_only_the_threshold ( ) {
354- let mut configuration = automation ( "b" , "m" , Some ( 14477 ) ) [ "configuration" ] . clone ( ) ;
404+ let mut configuration = automation ( "b" , "m" , "ll" , Some ( 14477 ) ) [ "configuration" ] . clone ( ) ;
355405 let expected = {
356406 let mut c = configuration. clone ( ) ;
357407 c[ "light_level" ] [ "daylight" ] [ "daylight_sensitivity" ] [ "settings" ] [ "dark_threshold" ] =
@@ -368,7 +418,7 @@ mod tests {
368418
369419 #[ test]
370420 fn set_dark_threshold_refuses_automation_without_daylight ( ) {
371- let mut configuration = automation ( "b" , "m" , None ) [ "configuration" ] . clone ( ) ;
421+ let mut configuration = automation ( "b" , "m" , "ll" , None ) [ "configuration" ] . clone ( ) ;
372422 let before = configuration. clone ( ) ;
373423
374424 assert ! ( !set_dark_threshold( & mut configuration, Some ( 20000 ) ) ) ;
0 commit comments