7676 ("-H" , 200 ): "heating_operation" ,
7777 ("=H" , 201 ): "hot_water_operation" ,
7878 ("0A" , 202 ): "anti_cycle_delay" ,
79+ ("0A" , 305 ): "dhw_post_heating_lockout" ,
7980 ("0H" , 203 ): "standby_no_heat_demand" ,
8081 ("0Y" , 204 ): "supply_temp_above_setpoint" ,
82+ ("0Y" , 359 ): "dhw_sensor_temp_too_high" ,
8183 ("" , 207 ): "system_pressure_too_low" ,
8284 ("A" , 208 ): "flue_gas_test_heat_demand" ,
8385 ("-A" , 208 ): "flue_gas_test_heat_demand" ,
8486 ("1C" , 210 ): "flue_gas_thermostat_or_air_pressure_active" ,
8587 ("2E" , 212 ): "safety_supply_temp_rising_too_fast" ,
8688 ("2P" , 212 ): "safety_supply_temp_rising_too_fast" ,
89+ ("2P" , 341 ): "heating_gradient_limitation" ,
90+ ("2P" , 342 ): "dhw_gradient_limitation" ,
8791 ("2U" , 213 ): "supply_return_temp_difference_too_high" ,
92+ ("3L" , 214 ): "fan_stopped_during_safety_time" ,
8893 ("3Y" , 214 ): "fan_stopped_during_safety_time" ,
8994 ("3Y" , 215 ): "fan_speed_too_high" ,
95+ ("3P" , 216 ): "fan_speed_too_low" ,
9096 ("3Y" , 216 ): "fan_malfunction" ,
91- ("3C" , 217 ): "air_pressure_monitor_fault" ,
97+ ("3A" , 264 ): "fan_stopped_during_operation" ,
98+ ("3F" , 273 ): "safety_shutdown_after_24h_continuous_operation" ,
99+ ("3C" , 217 ): "fan_malfunction" ,
92100 ("4A" , 218 ): "supply_temp_too_high" ,
93101 ("4F" , 219 ): "safety_sensor_temp_too_high" ,
94102 ("4U" , 220 ): "supply_sensor_short_circuit" ,
103+ ("4U" , 222 ): "supply_sensor_short_circuit" ,
104+ ("4U" , 350 ): "supply_sensor_short_circuit" ,
95105 ("4Y" , 221 ): "supply_sensor_disconnected" ,
106+ ("4Y" , 223 ): "supply_sensor_disconnected" ,
107+ ("4Y" , 351 ): "supply_sensor_disconnected" ,
96108 ("4C" , 222 ): "safety_sensor_short_circuit" ,
97109 ("4C" , 223 ): "safety_sensor_disconnected" ,
98110 ("4C" , 224 ): "safety_limiter_active" ,
99111 ("4L" , 225 ): "communication_or_internal_signal_fault" ,
100112 ("4L" , 226 ): "internal_monitoring_fault" ,
101113 ("6A" , 227 ): "no_flame_after_ignition" ,
102114 ("6C" , 228 ): "unexpected_flame_signal" ,
115+ ("6C" , 306 ): "flame_detected_after_gas_shutdown" ,
103116 ("6L" , 229 ): "ionization_signal_lost" ,
104117 ("6L" , 230 ): "invalid_ionization_signal" ,
105118 ("7C" , 231 ): "mains_voltage_fault_or_power_loss" ,
106119 ("8Y" , 232 ): "external_cutoff_switch_active" ,
107120 ("8Y" , 233 ): "external_controller_fault" ,
121+ ("7L" , 261 ): "first_safety_time_fault" ,
122+ ("7L" , 280 ): "restart_attempt_time_fault" ,
108123 ("9A" , 234 ): "gas_valve_communication_fault" ,
124+ ("9L" , 234 ): "gas_valve_communication_fault" ,
109125 ("9A" , 235 ): "gas_valve_not_recognized" ,
110126 ("9A" , 238 ): "internal_electronics_fault" ,
127+ ("9L" , 238 ): "internal_electronics_fault" ,
128+ ("9P" , 239 ): "kim_not_recognized" ,
111129 ("9U" , 239 ): "equipment_fault_relay_error" ,
112130 ("9U" , 240 ): "device_internal_fault" ,
113131 ("CU" , 240 ): "return_sensor_short_circuit" ,
120138 ("EC" , 257 ): "regulation_system_internal_fault" ,
121139 ("EC" , 258 ): "regulation_system_internal_fault" ,
122140 ("EC" , 259 ): "regulation_system_internal_fault" ,
141+ ("EL" , 259 ): "regulation_system_internal_fault" ,
123142 ("EA" , 260 ): "ignition_or_flame_fault" ,
124143 ("EA" , 261 ): "flame_fault" ,
125144 ("0E" , 265 ): "heat_demand_below_min_power" ,
132151 ("0Y" , 276 ): "supply_temp_above_setpoint" ,
133152 ("0U" , 280 ): "fan_starting" ,
134153 ("0U" , 281 ): "startup_delay" ,
154+ ("2Y" , 281 ): "pump_no_pressure_difference" ,
135155 ("2Y" , 282 ): "pump_or_flow_insufficient" ,
156+ ("2E" , 357 ): "purge_function_active" ,
157+ ("2H" , 358 ): "pump_or_three_way_valve_anti_seizure" ,
136158 ("0C" , 283 ): "burner_starting" ,
137159 ("0L" , 284 ): "gas_valve_opening" ,
138160 ("0L" , 285 ): "gas_valve_open_burner_startup" ,
147169 ("EL" , 299 ): "internal_system_fault" ,
148170}
149171
150- _APPLIANCE_STATUS_BY_CAUSE : dict [int , str ] = {
151- 200 : "heating_operation" ,
152- 201 : "hot_water_operation" ,
153- 202 : "anti_cycle_delay" ,
154- 203 : "standby_no_heat_demand" ,
155- 204 : "supply_temp_above_setpoint" ,
156- 207 : "system_pressure_too_low" ,
157- 208 : "flue_gas_test_heat_demand" ,
158- 210 : "flue_gas_thermostat_or_air_pressure_active" ,
159- 212 : "safety_supply_temp_rising_too_fast" ,
160- 213 : "supply_return_temp_difference_too_high" ,
161- 214 : "fan_stopped_during_safety_time" ,
162- 215 : "fan_speed_too_high" ,
163- 216 : "fan_malfunction" ,
164- 217 : "air_pressure_monitor_fault" ,
165- 218 : "supply_temp_too_high" ,
166- 219 : "safety_sensor_temp_too_high" ,
167- 220 : "supply_sensor_short_circuit" ,
168- 221 : "supply_sensor_disconnected" ,
169- 222 : "safety_sensor_short_circuit" ,
170- 223 : "safety_sensor_disconnected" ,
171- 224 : "safety_limiter_active" ,
172- 225 : "communication_or_internal_signal_fault" ,
173- 226 : "internal_monitoring_fault" ,
174- 227 : "no_flame_after_ignition" ,
175- 228 : "unexpected_flame_signal" ,
176- 229 : "ionization_signal_lost" ,
177- 230 : "invalid_ionization_signal" ,
178- 231 : "mains_voltage_fault_or_power_loss" ,
179- 232 : "external_cutoff_switch_active" ,
180- 233 : "external_controller_fault" ,
181- 234 : "gas_valve_communication_fault" ,
182- 235 : "gas_valve_not_recognized" ,
183- 238 : "internal_electronics_fault" ,
184- 239 : "equipment_fault_relay_error" ,
185- 240 : "device_internal_fault" ,
186- 241 : "return_sensor_short_circuit" ,
187- 242 : "return_sensor_disconnected" ,
188- 243 : "internal_communication_fault" ,
189- 244 : "sensor_or_electronics_fault" ,
190- 245 : "communication_fault" ,
191- 246 : "internal_controller_fault" ,
192- 257 : "regulation_system_internal_fault" ,
193- 258 : "regulation_system_internal_fault" ,
194- 259 : "regulation_system_internal_fault" ,
195- 260 : "ignition_or_flame_fault" ,
196- 261 : "flame_fault" ,
197- 265 : "heat_demand_below_min_power" ,
198- 268 : "regulation_system_test" ,
199- 270 : "boiler_starting" ,
200- 271 : "boiler_startup_preventilation" ,
201- 272 : "burner_operation_monitoring" ,
202- 273 : "flame_monitoring" ,
203- 274 : "burner_startup_phase" ,
204- 276 : "supply_temp_above_setpoint" ,
205- 280 : "fan_starting" ,
206- 281 : "startup_delay" ,
207- 282 : "pump_or_flow_insufficient" ,
208- 283 : "burner_starting" ,
209- 284 : "gas_valve_opening" ,
210- 285 : "gas_valve_open_burner_startup" ,
211- 290 : "main_controller_fault" ,
212- 291 : "internal_electronics_fault" ,
213- 292 : "regulation_system_fault" ,
214- 293 : "internal_fault" ,
215- 294 : "electronic_fault" ,
216- 296 : "internal_controller_fault" ,
217- 297 : "internal_fault" ,
218- 298 : "electronic_fault" ,
219- 299 : "internal_system_fault" ,
220- }
172+ def _build_appliance_status_by_cause (
173+ by_code : dict [tuple [str , int ], str ],
174+ ) -> dict [int , str ]:
175+ """Build a cause-only fallback map from the pair table.
176+
177+ Keep the first seen mapping for a cause code to preserve the intended
178+ primary meaning when multiple display codes share the same cause.
179+ """
180+
181+ by_cause : dict [int , str ] = {}
182+ for (_display , cause ), status in by_code .items ():
183+ by_cause .setdefault (cause , status )
184+ return by_cause
185+
186+
187+ _APPLIANCE_STATUS_BY_CAUSE : dict [int , str ] = _build_appliance_status_by_cause (
188+ _APPLIANCE_STATUS_BY_CODE
189+ )
221190
222191
223192def _val (data : dict [str , Any ], path : str , key : str = VALUE_KEY ) -> Any :
@@ -876,15 +845,72 @@ def _appliance_cause_code(data: dict[str, Any]) -> int | None:
876845 return None
877846
878847
848+ def _appliance_error_flag (data : dict [str , Any ], path : str ) -> bool | None :
849+ """Parse appliance error flags that may be bool, numeric or string values."""
850+ raw = _val (data , path )
851+ if isinstance (raw , bool ):
852+ return raw
853+ if isinstance (raw , (int , float )):
854+ return raw != 0
855+ if isinstance (raw , str ):
856+ value = raw .strip ().lower ()
857+ if value in {"true" , "1" , "on" , "yes" }:
858+ return True
859+ if value in {"false" , "0" , "off" , "no" }:
860+ return False
861+ return None
862+
863+
864+ def _appliance_optional_code (data : dict [str , Any ], path : str ) -> int | None :
865+ """Parse optional diagnostic codes (service/blocking/locking) when numeric."""
866+ raw = _val (data , path )
867+ if raw is None or isinstance (raw , bool ):
868+ return None
869+ if isinstance (raw , (int , float )):
870+ return int (raw )
871+ if isinstance (raw , str ):
872+ value = raw .strip ().lower ()
873+ if value in {"" , "true" , "false" , "on" , "off" , "yes" , "no" }:
874+ return None
875+ try :
876+ return int (float (value ))
877+ except ValueError :
878+ return None
879+ return None
880+
881+
879882def _appliance_status_state (data : dict [str , Any ]) -> str | None :
880883 display = _appliance_display_code (data )
881884 cause = _appliance_cause_code (data )
882- if display is None and cause is None :
883- return None
885+ blocking = _appliance_error_flag (data , "/system/appliance/blockingError" )
886+ locking = _appliance_error_flag (data , "/system/appliance/lockingError" )
887+ blocking_code = _appliance_optional_code (data , "/system/appliance/blockingError" )
888+ locking_code = _appliance_optional_code (data , "/system/appliance/lockingError" )
889+
890+ # Single mapping table, context-aware lookup order:
891+ # locking pair -> blocking pair -> service pair.
892+ if locking is True :
893+ if display is not None and locking_code is not None :
894+ status = _APPLIANCE_STATUS_BY_CODE .get ((display , locking_code ))
895+ if status is not None :
896+ return status
897+ return "locking_fault_code_active"
898+
899+ if blocking is True :
900+ if display is not None and blocking_code is not None :
901+ status = _APPLIANCE_STATUS_BY_CODE .get ((display , blocking_code ))
902+ if status is not None :
903+ return status
904+ return "blocking_fault_code_active"
905+
884906 if display is not None and cause is not None :
885907 status = _APPLIANCE_STATUS_BY_CODE .get ((display , cause ))
886908 if status is not None :
887909 return status
910+
911+ if display is None and cause is None and blocking is None and locking is None :
912+ return None
913+
888914 if cause is not None :
889915 status = _APPLIANCE_STATUS_BY_CAUSE .get (cause )
890916 if status is not None :
@@ -897,17 +923,32 @@ def _appliance_status_state(data: dict[str, Any]) -> str | None:
897923def _appliance_status_attributes (data : dict [str , Any ]) -> dict [str , Any ] | None :
898924 display = _appliance_display_code (data )
899925 cause = _appliance_cause_code (data )
900- if display is None and cause is None :
926+ blocking = _appliance_error_flag (data , "/system/appliance/blockingError" )
927+ locking = _appliance_error_flag (data , "/system/appliance/lockingError" )
928+ blocking_code = _appliance_optional_code (data , "/system/appliance/blockingError" )
929+ locking_code = _appliance_optional_code (data , "/system/appliance/lockingError" )
930+ if display is None and cause is None and blocking is None and locking is None :
901931 return None
902- return {
932+ attrs : dict [ str , Any ] = {
903933 "display_code" : display ,
904934 "cause_code" : cause ,
905935 }
936+ if blocking is not None :
937+ attrs ["blocking_error" ] = blocking
938+ if blocking_code is not None :
939+ attrs ["blocking_code" ] = blocking_code
940+ if locking is not None :
941+ attrs ["locking_error" ] = locking
942+ if locking_code is not None :
943+ attrs ["locking_code" ] = locking_code
944+ return attrs
906945
907946
908947def _appliance_status_available (data : dict [str , Any ]) -> bool :
909948 return _val (data , "/system/appliance/displayCode" ) is not None or _val (
910949 data , "/system/appliance/causeCode"
950+ ) is not None or _val (data , "/system/appliance/blockingError" ) is not None or _val (
951+ data , "/system/appliance/lockingError"
911952 ) is not None
912953
913954
0 commit comments