@@ -2312,14 +2312,55 @@ def test_api_get_scheduler_v2_evo(my_predbat):
23122312 assert group ["maxSoc" ] == 100.0
23132313 assert "extraParam" not in group
23142314
2315- # v2 has no properties block: fall back to defaults, capped at inverter capacity
2315+ # This mocked response has no properties block: fall back to defaults, capped at capacity
23162316 assert fox .fdpwr_max [deviceSN ] == 8000
23172317 assert fox .fdsoc_min [deviceSN ] == 10
23182318 assert fox .device_scheduler_count [deviceSN ] == 1
23192319
23202320 return False
23212321
23222322
2323+ def test_api_get_scheduler_v2_uses_real_properties (my_predbat ):
2324+ """
2325+ Test get_scheduler uses the real per-field ranges from a v2 response's properties block
2326+ instead of the generic defaults, when the device actually returns one.
2327+
2328+ Regression guard: get_scheduler_v2 used to discard the properties block entirely on the
2329+ (incorrect) assumption that v2 never returns one - production responses do include it.
2330+ """
2331+ print (" - test_api_get_scheduler_v2_uses_real_properties" )
2332+
2333+ fox = MockFoxAPIWithRequests ()
2334+ deviceSN = "EVO1234567"
2335+ fox .device_detail [deviceSN ] = {"hasBattery" : True , "capacity" : 20 , "productType" : "812" }
2336+
2337+ fox .set_mock_response (
2338+ "/op/v2/device/scheduler/get" ,
2339+ {
2340+ "enable" : 1 ,
2341+ "groups" : [
2342+ {"enable" : 1 , "startHour" : 0 , "endHour" : 5 , "workMode" : "ForceCharge" , "extraParam" : {"fdPwr" : 5000.0 , "fdSoc" : 100.0 }},
2343+ ],
2344+ "properties" : {
2345+ "fdpwr" : {"unit" : "W" , "precision" : 1.0 , "range" : {"min" : 0.0 , "max" : 12000.0 }},
2346+ "fdsoc" : {"unit" : "%" , "precision" : 1.0 , "range" : {"min" : 5.0 , "max" : 100.0 }},
2347+ "exportlimit" : {"unit" : "W" , "precision" : 1.0 , "range" : {"min" : 0.0 , "max" : 100000.0 }},
2348+ },
2349+ },
2350+ )
2351+
2352+ result = asyncio .run (fox .get_scheduler (deviceSN ))
2353+
2354+ # Real reported max (12000) must win over the generic 8000 default
2355+ assert fox .fdpwr_max [deviceSN ] == 12000
2356+ # Real reported min (5) must win over the generic 10 default
2357+ assert fox .fdsoc_min [deviceSN ] == 5
2358+ # The properties block itself must be preserved on the result, not discarded
2359+ assert result ["properties" ]["exportlimit" ]["range" ] == {"min" : 0.0 , "max" : 100000.0 }
2360+
2361+ return False
2362+
2363+
23232364def test_api_get_scheduler_derives_settings_from_schedule (my_predbat ):
23242365 """
23252366 Test get_scheduler derives ExportLimit/ImportLimit/MaxSoc/PvLimit (max) and MinSocOnGrid
@@ -2378,6 +2419,88 @@ def test_api_get_scheduler_derives_settings_from_schedule(my_predbat):
23782419 return False
23792420
23802421
2422+ def test_api_get_scheduler_derives_settings_with_range_from_properties (my_predbat ):
2423+ """
2424+ Test update_settings_from_schedule attaches the real range/unit/precision from a v2
2425+ response's properties block to a derived setting, not just a bare value.
2426+
2427+ Regression guard: without this, ExportLimit derived purely as {"value": ...} publishes as
2428+ a sensor (no range/enumList), but automatic_config wires export_limit to a hardcoded
2429+ number.*_setting_exportlimit entity id - leaving that entity unresolvable (HA reports
2430+ None) even though Predbat believes it configured a working export limit.
2431+ """
2432+ print (" - test_api_get_scheduler_derives_settings_with_range_from_properties" )
2433+
2434+ fox = MockFoxAPIWithRequests ()
2435+ deviceSN = "EVO1234567"
2436+ fox .device_detail [deviceSN ] = {"hasBattery" : True , "capacity" : 20 , "productType" : "812" }
2437+
2438+ fox .set_mock_response (
2439+ "/op/v2/device/scheduler/get" ,
2440+ {
2441+ "enable" : 1 ,
2442+ "groups" : [
2443+ {"enable" : 1 , "startHour" : 0 , "endHour" : 23 , "workMode" : "SelfUse" , "extraParam" : {"fdPwr" : 5000.0 , "fdSoc" : 10.0 , "exportLimit" : 12000.0 , "minSocOnGrid" : 10.0 , "maxSoc" : 100.0 }},
2444+ ],
2445+ "properties" : {
2446+ "exportlimit" : {"unit" : "W" , "precision" : 1.0 , "range" : {"min" : 0.0 , "max" : 100000.0 }},
2447+ },
2448+ },
2449+ )
2450+
2451+ asyncio .run (fox .get_scheduler (deviceSN ))
2452+
2453+ export_limit_setting = fox .device_settings [deviceSN ]["ExportLimit" ]
2454+ assert export_limit_setting ["value" ] == 12000.0
2455+ assert export_limit_setting ["range" ] == {"min" : 0.0 , "max" : 100000.0 }
2456+ assert export_limit_setting ["unit" ] == "W"
2457+ assert export_limit_setting ["precision" ] == 1.0
2458+
2459+ # A field with no matching properties entry (maxSoc here) still just gets a bare value
2460+ assert fox .device_settings [deviceSN ]["MaxSoc" ] == {"value" : 100.0 }
2461+
2462+ return False
2463+
2464+
2465+ def test_publish_data_derived_export_limit_publishes_as_number (my_predbat ):
2466+ """
2467+ End-to-end regression guard: a schedule-derived ExportLimit with real range metadata must
2468+ publish as a number entity, matching the hardcoded number.*_setting_exportlimit entity id
2469+ automatic_config wires export_limit to - not a sensor, which would leave that entity
2470+ unresolvable and fail apps.yaml validation (HA reports state None).
2471+ """
2472+ print (" - test_publish_data_derived_export_limit_publishes_as_number" )
2473+
2474+ fox = MockFoxAPIWithRequests ()
2475+ deviceSN = "TEST123456"
2476+
2477+ fox .device_list = [{"deviceSN" : deviceSN }]
2478+ fox .device_detail [deviceSN ] = {"hasPV" : True , "hasBattery" : True , "capacity" : 8 , "function" : {}, "deviceType" : "KH8" , "stationName" : "Test" , "batteryList" : []}
2479+ fox .fdpwr_max [deviceSN ] = 8000
2480+ fox .fdsoc_min [deviceSN ] = 10
2481+ fox .device_values [deviceSN ] = {}
2482+ fox .local_schedule [deviceSN ] = {}
2483+
2484+ # Simulate a schedule-derived ExportLimit, with range metadata from a real properties block
2485+ fox .update_settings_from_schedule (
2486+ deviceSN ,
2487+ [{"exportLimit" : 12000.0 }],
2488+ {"exportlimit" : {"unit" : "W" , "precision" : 1.0 , "range" : {"min" : 0.0 , "max" : 100000.0 }}},
2489+ )
2490+
2491+ run_async (fox .publish_data ())
2492+
2493+ export_limit_entity = f"number.predbat_fox_{ deviceSN .lower ()} _setting_exportlimit"
2494+ assert export_limit_entity in fox .dashboard_items
2495+ assert fox .dashboard_items [export_limit_entity ]["state" ] == 12000.0
2496+ assert fox .dashboard_items [export_limit_entity ]["attributes" ]["max" ] == 100000.0
2497+
2498+ # Must NOT have also published as a sensor
2499+ assert f"sensor.predbat_fox_{ deviceSN .lower ()} _setting_exportlimit" not in fox .dashboard_items
2500+
2501+ return False
2502+
2503+
23812504def test_api_get_scheduler_v2_null_groups (my_predbat ):
23822505 """
23832506 Test get_scheduler_v2 tolerates a present-but-null groups value and defaults enable
@@ -6538,7 +6661,9 @@ def run_fox_api_tests(my_predbat):
65386661 failed |= test_api_set_battery_charging_time (my_predbat )
65396662 failed |= test_api_get_scheduler (my_predbat )
65406663 failed |= test_api_get_scheduler_v2_evo (my_predbat )
6664+ failed |= test_api_get_scheduler_v2_uses_real_properties (my_predbat )
65416665 failed |= test_api_get_scheduler_derives_settings_from_schedule (my_predbat )
6666+ failed |= test_api_get_scheduler_derives_settings_with_range_from_properties (my_predbat )
65426667 failed |= test_api_get_scheduler_v2_null_groups (my_predbat )
65436668 failed |= test_api_get_scheduler_kh_stays_v1 (my_predbat )
65446669 failed |= test_api_get_scheduler_v2_evo_fails (my_predbat )
@@ -6658,6 +6783,7 @@ def run_fox_api_tests(my_predbat):
66586783 failed |= test_publish_data_device_values_dual_soc (my_predbat )
66596784 failed |= test_publish_data_device_settings (my_predbat )
66606785 failed |= test_publish_data_workmode_default_publishes_as_select (my_predbat )
6786+ failed |= test_publish_data_derived_export_limit_publishes_as_number (my_predbat )
66616787 failed |= test_publish_data_no_battery_skips_settings (my_predbat )
66626788
66636789 # apply_battery_schedule tests
0 commit comments