@@ -1558,6 +1558,31 @@ def test_api_get_device_detail(my_predbat):
15581558 return False
15591559
15601560
1561+ def test_capacity_watts_half_kw_device_type (my_predbat ):
1562+ """
1563+ Test capacity_watts() corrects the truncated 'capacity' field for half-kW models.
1564+
1565+ Fox's device/detail 'capacity' field is an integer, so a 10.5kW KH10.5 inverter
1566+ reports capacity=10. Only a deviceType ending in '.5' with a whole-kW capacity
1567+ should be bumped up to end in 500; anything else passes through unchanged.
1568+ """
1569+ print (" - test_capacity_watts_half_kw_device_type" )
1570+
1571+ # Whole-kW model is untouched
1572+ assert FoxAPI .capacity_watts ({"capacity" : 8 , "deviceType" : "KH8" }) == 8000
1573+ # Half-kW model gets bumped from a truncated whole-kW capacity
1574+ assert FoxAPI .capacity_watts ({"capacity" : 10 , "deviceType" : "KH10.5" }) == 10500
1575+ # Missing/unknown deviceType is left alone
1576+ assert FoxAPI .capacity_watts ({"capacity" : 10 , "deviceType" : "Unknown" }) == 10000
1577+ assert FoxAPI .capacity_watts ({"capacity" : 10 }) == 10000
1578+ # Zero capacity is left alone even with a half-kW deviceType
1579+ assert FoxAPI .capacity_watts ({"capacity" : 0 , "deviceType" : "KH10.5" }) == 0
1580+ # A capacity that isn't a whole kW multiple is left alone (already carries a fraction)
1581+ assert FoxAPI .capacity_watts ({"capacity" : 10.3 , "deviceType" : "KH10.5" }) == 10300
1582+
1583+ return False
1584+
1585+
15611586def test_api_get_device_history (my_predbat ):
15621587 """
15631588 Test get_device_history API endpoint
@@ -2191,6 +2216,72 @@ def test_api_get_scheduler(my_predbat):
21912216 return False
21922217
21932218
2219+ def test_api_get_scheduler_half_kw_capacity (my_predbat ):
2220+ """
2221+ Test get_scheduler doesn't clamp fdpwr_max below a genuine half-kW rating.
2222+
2223+ Fox's device/detail 'capacity' field is an integer, so a 10.5kW KH10.5 inverter reports
2224+ capacity=10 even though the scheduler API correctly reports a 10500W fdpwr max. The clamp
2225+ uses capacity_watts(), which corrects the half-kW deviceType before capping, so it should
2226+ still clamp to the true 10500W rather than the truncated 10000W.
2227+ """
2228+ print (" - test_api_get_scheduler_half_kw_capacity" )
2229+
2230+ fox = MockFoxAPIWithRequests ()
2231+ deviceSN = "TEST123456"
2232+
2233+ fox .device_detail [deviceSN ] = {"hasBattery" : True , "capacity" : 10 , "deviceType" : "KH10.5" }
2234+
2235+ fox .set_mock_response (
2236+ "/op/v1/device/scheduler/get" ,
2237+ {
2238+ "enable" : 1 ,
2239+ "groups" : [],
2240+ "properties" : {
2241+ "fdpwr" : {"unit" : "W" , "precision" : 1.0 , "range" : {"min" : 0.0 , "max" : 10500.0 }},
2242+ "fdsoc" : {"unit" : "%" , "precision" : 1.0 , "range" : {"min" : 10.0 , "max" : 100.0 }},
2243+ },
2244+ },
2245+ )
2246+
2247+ asyncio .run (fox .get_scheduler (deviceSN ))
2248+
2249+ assert fox .fdpwr_max [deviceSN ] == 10500
2250+
2251+ return False
2252+
2253+
2254+ def test_api_get_scheduler_still_clamps_bogus_fdpwr (my_predbat ):
2255+ """
2256+ Test get_scheduler still clamps a genuinely bogus fdpwr max that exceeds the
2257+ device's rated capacity.
2258+ """
2259+ print (" - test_api_get_scheduler_still_clamps_bogus_fdpwr" )
2260+
2261+ fox = MockFoxAPIWithRequests ()
2262+ deviceSN = "TEST123456"
2263+
2264+ fox .device_detail [deviceSN ] = {"hasBattery" : True , "capacity" : 8 }
2265+
2266+ fox .set_mock_response (
2267+ "/op/v1/device/scheduler/get" ,
2268+ {
2269+ "enable" : 1 ,
2270+ "groups" : [],
2271+ "properties" : {
2272+ "fdpwr" : {"unit" : "W" , "precision" : 1.0 , "range" : {"min" : 0.0 , "max" : 32000.0 }},
2273+ "fdsoc" : {"unit" : "%" , "precision" : 1.0 , "range" : {"min" : 10.0 , "max" : 100.0 }},
2274+ },
2275+ },
2276+ )
2277+
2278+ asyncio .run (fox .get_scheduler (deviceSN ))
2279+
2280+ assert fox .fdpwr_max [deviceSN ] == 8000
2281+
2282+ return False
2283+
2284+
21942285def test_api_set_scheduler (my_predbat ):
21952286 """
21962287 Test set_scheduler API endpoint
@@ -5515,6 +5606,46 @@ def test_publish_data_device_info(my_predbat):
55155606 return False
55165607
55175608
5609+ def test_publish_data_device_info_half_kw_capacity (my_predbat ):
5610+ """
5611+ Test publish_data corrects a half-kW model's truncated capacity.
5612+
5613+ Fox reports the device/detail 'capacity' field as an integer, so a 10.5kW KH10.5
5614+ inverter reports capacity=10. deviceType ending in '.5' with a whole-kW capacity
5615+ should bump the reported watts up to end in 500 rather than 000.
5616+ """
5617+ print (" - test_publish_data_device_info_half_kw_capacity" )
5618+
5619+ fox = MockFoxAPIWithRequests ()
5620+ deviceSN = "TEST123456"
5621+
5622+ fox .device_list = [{"deviceSN" : deviceSN }]
5623+ fox .device_detail [deviceSN ] = {
5624+ "hasPV" : True ,
5625+ "hasBattery" : True ,
5626+ "capacity" : 10 ,
5627+ "function" : {"scheduler" : True },
5628+ "deviceType" : "KH10.5" ,
5629+ "stationName" : "Test Home" ,
5630+ "batteryList" : [{"capacity" : 10360 }],
5631+ }
5632+ fox .fdpwr_max [deviceSN ] = 10500
5633+ fox .fdsoc_min [deviceSN ] = 10
5634+ fox .device_values [deviceSN ] = {}
5635+ fox .device_settings [deviceSN ] = {}
5636+ fox .local_schedule [deviceSN ] = {}
5637+
5638+ run_async (fox .publish_data ())
5639+
5640+ info_entity = f"sensor.predbat_fox_{ deviceSN .lower ()} _info"
5641+ assert fox .dashboard_items [info_entity ]["attributes" ]["inverterCapacity" ] == 10500
5642+
5643+ inverter_capacity_entity = f"sensor.predbat_fox_{ deviceSN .lower ()} _inverter_capacity"
5644+ assert fox .dashboard_items [inverter_capacity_entity ]["state" ] == 10500
5645+
5646+ return False
5647+
5648+
55185649def test_publish_data_battery_soh (my_predbat ):
55195650 """
55205651 Test publish_data creates battery_soh sensor with correct value and attributes
@@ -5647,6 +5778,42 @@ def test_publish_data_device_values_dual_soc(my_predbat):
56475778 return False
56485779
56495780
5781+ def test_publish_data_production_today_and_month_rounded (my_predbat ):
5782+ """
5783+ Test publish_data rounds the '_today' and '_month' production sensors to 2dp.
5784+
5785+ Fox's daily history values carry floating-point summation artifacts (e.g.
5786+ 33.30000000000109), which without rounding leak through to the today total
5787+ directly and compound further when summed for the month total.
5788+ """
5789+ print (" - test_publish_data_production_today_and_month_rounded" )
5790+
5791+ fox = MockFoxAPIWithRequests ()
5792+ deviceSN = "TEST123456"
5793+
5794+ fox .device_list = [{"deviceSN" : deviceSN }]
5795+ fox .device_detail [deviceSN ] = {"hasPV" : True , "hasBattery" : True , "capacity" : 8 , "function" : {}, "deviceType" : "KH8" , "stationName" : "Test" , "batteryList" : []}
5796+ fox .fdpwr_max [deviceSN ] = 8000
5797+ fox .fdsoc_min [deviceSN ] = 10
5798+ fox .device_values [deviceSN ] = {}
5799+ fox .device_settings [deviceSN ] = {}
5800+ fox .local_schedule [deviceSN ] = {}
5801+
5802+ # Every day carries the same artifact-laden value so the assertion doesn't depend on today's date
5803+ daily_value = 33.30000000000109
5804+ fox .device_production_month [deviceSN ] = [{"unit" : "kWh" , "variable" : "generation" , "values" : [daily_value ] * 31 }]
5805+
5806+ run_async (fox .publish_data ())
5807+
5808+ today_entity = f"sensor.predbat_fox_{ deviceSN .lower ()} _generation_today"
5809+ assert fox .dashboard_items [today_entity ]["state" ] == 33.3 , f"Expected 33.3 but got { fox .dashboard_items [today_entity ]['state' ]} "
5810+
5811+ month_entity = f"sensor.predbat_fox_{ deviceSN .lower ()} _generation_month"
5812+ assert fox .dashboard_items [month_entity ]["state" ] == round (31 * daily_value , 2 ), f"Got { fox .dashboard_items [month_entity ]['state' ]} "
5813+
5814+ return False
5815+
5816+
56505817def test_publish_data_device_settings (my_predbat ):
56515818 """
56525819 Test publish_data creates settings entities correctly
@@ -6737,6 +6904,7 @@ def run_fox_api_tests(my_predbat):
67376904 # API endpoint tests with mocked request_get
67386905 failed |= test_api_get_device_list (my_predbat )
67396906 failed |= test_api_get_device_detail (my_predbat )
6907+ failed |= test_capacity_watts_half_kw_device_type (my_predbat )
67406908 failed |= test_api_get_device_history (my_predbat )
67416909 failed |= test_api_get_device_history_empty (my_predbat )
67426910 failed |= test_api_get_available_variables (my_predbat )
@@ -6755,6 +6923,8 @@ def run_fox_api_tests(my_predbat):
67556923 failed |= test_api_get_battery_charging_time (my_predbat )
67566924 failed |= test_api_set_battery_charging_time (my_predbat )
67576925 failed |= test_api_get_scheduler (my_predbat )
6926+ failed |= test_api_get_scheduler_half_kw_capacity (my_predbat )
6927+ failed |= test_api_get_scheduler_still_clamps_bogus_fdpwr (my_predbat )
67586928 failed |= test_api_get_scheduler_v2_evo (my_predbat )
67596929 failed |= test_api_get_scheduler_v2_uses_real_properties (my_predbat )
67606930 failed |= test_api_get_scheduler_derives_settings_from_schedule (my_predbat )
@@ -6875,9 +7045,11 @@ def run_fox_api_tests(my_predbat):
68757045
68767046 # publish_data tests
68777047 failed |= test_publish_data_device_info (my_predbat )
7048+ failed |= test_publish_data_device_info_half_kw_capacity (my_predbat )
68787049 failed |= test_publish_data_battery_soh (my_predbat )
68797050 failed |= test_publish_data_device_values (my_predbat )
68807051 failed |= test_publish_data_device_values_dual_soc (my_predbat )
7052+ failed |= test_publish_data_production_today_and_month_rounded (my_predbat )
68817053 failed |= test_publish_data_device_settings (my_predbat )
68827054 failed |= test_publish_data_workmode_default_publishes_as_select (my_predbat )
68837055 failed |= test_publish_data_derived_export_limit_publishes_as_number (my_predbat )
0 commit comments