revised this gist 1 week ago. Go to revision
1 file changed, 469 insertions
izzyfast.yaml(file created)
| @@ -0,0 +1,469 @@ | |||
| 1 | + | substitutions: | |
| 2 | + | device_name: "izzifast" | |
| 3 | + | friendly_name: "IZZI Fast Heat Recovery Unit" | |
| 4 | + | default_max_volume: "23" # Default 23 (230 m³/h), e.g. 30 for 300 m³/h unit | |
| 5 | + | web_username: "admin" | |
| 6 | + | web_password: "XXX" | |
| 7 | + | api_encryption_key: "XXX" | |
| 8 | + | ota_password: "XXX" | |
| 9 | + | ||
| 10 | + | esphome: | |
| 11 | + | name: "${device_name}" | |
| 12 | + | friendly_name: "${friendly_name}" | |
| 13 | + | ||
| 14 | + | esp32: | |
| 15 | + | board: esp32dev | |
| 16 | + | framework: | |
| 17 | + | type: esp-idf | |
| 18 | + | ||
| 19 | + | # WiFi configuration | |
| 20 | + | wifi: | |
| 21 | + | ssid: "XXX" | |
| 22 | + | password: "XXX" | |
| 23 | + | ||
| 24 | + | # Optional fallback AP if WiFi is unreachable | |
| 25 | + | ap: | |
| 26 | + | ssid: "${device_name}_fallback" | |
| 27 | + | password: "fallback_password" | |
| 28 | + | ||
| 29 | + | captive_portal: | |
| 30 | + | ||
| 31 | + | # Web Server interface with authentication and live log streaming | |
| 32 | + | web_server: | |
| 33 | + | port: 80 | |
| 34 | + | auth: | |
| 35 | + | username: "${web_username}" | |
| 36 | + | password: "${web_password}" | |
| 37 | + | ||
| 38 | + | # Native Home Assistant API with noise encryption and OTA updates | |
| 39 | + | api: | |
| 40 | + | encryption: | |
| 41 | + | key: "${api_encryption_key}" | |
| 42 | + | ||
| 43 | + | ota: | |
| 44 | + | - platform: esphome | |
| 45 | + | password: "${ota_password}" | |
| 46 | + | ||
| 47 | + | logger: | |
| 48 | + | level: INFO | |
| 49 | + | ||
| 50 | + | # Hardware UART (Serial2) in hardware RS-485 Half-Duplex mode | |
| 51 | + | uart: | |
| 52 | + | id: uart_bus | |
| 53 | + | tx_pin: GPIO17 | |
| 54 | + | rx_pin: GPIO16 | |
| 55 | + | baud_rate: 9600 | |
| 56 | + | data_bits: 8 | |
| 57 | + | parity: NONE | |
| 58 | + | stop_bits: 1 | |
| 59 | + | flow_control_pin: GPIO27 # Automatic hardware DE/RE transceiver control (ESP-IDF) | |
| 60 | + | ||
| 61 | + | # ============================================================================== | |
| 62 | + | # TELEMETRY SENSORS | |
| 63 | + | # ============================================================================== | |
| 64 | + | sensor: | |
| 65 | + | # 1. Outdoor intake temperature | |
| 66 | + | - platform: template | |
| 67 | + | name: "Outdoor Temperature" | |
| 68 | + | id: s_outdoor_temp | |
| 69 | + | unit_of_measurement: "°C" | |
| 70 | + | device_class: temperature | |
| 71 | + | state_class: measurement | |
| 72 | + | accuracy_decimals: 0 | |
| 73 | + | icon: "mdi:weather-snowy-heavy" | |
| 74 | + | ||
| 75 | + | # 2. Exhaust outlet temperature | |
| 76 | + | - platform: template | |
| 77 | + | name: "Exhaust Temperature" | |
| 78 | + | id: s_exhaust_temp | |
| 79 | + | unit_of_measurement: "°C" | |
| 80 | + | device_class: temperature | |
| 81 | + | state_class: measurement | |
| 82 | + | accuracy_decimals: 0 | |
| 83 | + | icon: "mdi:weather-windy" | |
| 84 | + | ||
| 85 | + | # 3. Supply air temperature (into rooms) | |
| 86 | + | - platform: template | |
| 87 | + | name: "Supply Temperature" | |
| 88 | + | id: s_supply_temp | |
| 89 | + | unit_of_measurement: "°C" | |
| 90 | + | device_class: temperature | |
| 91 | + | state_class: measurement | |
| 92 | + | accuracy_decimals: 0 | |
| 93 | + | icon: "mdi:air-filter" | |
| 94 | + | ||
| 95 | + | # 4. Extract air temperature (from rooms) | |
| 96 | + | - platform: template | |
| 97 | + | name: "Extract Temperature" | |
| 98 | + | id: s_extract_temp | |
| 99 | + | unit_of_measurement: "°C" | |
| 100 | + | device_class: temperature | |
| 101 | + | state_class: measurement | |
| 102 | + | accuracy_decimals: 0 | |
| 103 | + | icon: "mdi:home-thermometer" | |
| 104 | + | ||
| 105 | + | # 5. Calculated heat recovery efficiency | |
| 106 | + | - platform: template | |
| 107 | + | name: "Heat Recovery Efficiency" | |
| 108 | + | id: s_efficiency | |
| 109 | + | unit_of_measurement: "%" | |
| 110 | + | state_class: measurement | |
| 111 | + | accuracy_decimals: 1 | |
| 112 | + | icon: "mdi:percent" | |
| 113 | + | ||
| 114 | + | # 6. Current supply air volume (m³/h) | |
| 115 | + | - platform: template | |
| 116 | + | name: "Supply Air Volume" | |
| 117 | + | id: s_supply_volume | |
| 118 | + | unit_of_measurement: "m³/h" | |
| 119 | + | state_class: measurement | |
| 120 | + | accuracy_decimals: 0 | |
| 121 | + | icon: "mdi:fan-plus" | |
| 122 | + | ||
| 123 | + | # 7. Current extract air volume (m³/h) | |
| 124 | + | - platform: template | |
| 125 | + | name: "Extract Air Volume" | |
| 126 | + | id: s_extract_volume | |
| 127 | + | unit_of_measurement: "m³/h" | |
| 128 | + | state_class: measurement | |
| 129 | + | accuracy_decimals: 0 | |
| 130 | + | icon: "mdi:fan-minus" | |
| 131 | + | ||
| 132 | + | # ============================================================================== | |
| 133 | + | # BINARY SENSORS (LOGICAL STATES) | |
| 134 | + | # ============================================================================== | |
| 135 | + | binary_sensor: | |
| 136 | + | # Physical bypass damper state | |
| 137 | + | - platform: template | |
| 138 | + | name: "Bypass Open" | |
| 139 | + | id: bs_bypass_state | |
| 140 | + | device_class: opening | |
| 141 | + | icon: "mdi:swap-horizontal" | |
| 142 | + | ||
| 143 | + | # Housing cover / filter flap open alert (Byte 5 == 5) | |
| 144 | + | - platform: template | |
| 145 | + | name: "Cover Open" | |
| 146 | + | id: bs_cover_open | |
| 147 | + | device_class: problem | |
| 148 | + | icon: "mdi:door-open" | |
| 149 | + | ||
| 150 | + | # Active heat exchanger defrosting (Byte 5 == 10) | |
| 151 | + | - platform: template | |
| 152 | + | name: "Defrost Active" | |
| 153 | + | id: bs_defrost_active | |
| 154 | + | device_class: heat | |
| 155 | + | icon: "mdi:snowflake-melt" | |
| 156 | + | ||
| 157 | + | # ============================================================================== | |
| 158 | + | # GLOBAL STATE & CONTROL VARIABLES | |
| 159 | + | # ============================================================================== | |
| 160 | + | globals: | |
| 161 | + | - id: g_target_supply_speed | |
| 162 | + | type: int | |
| 163 | + | initial_value: "30" | |
| 164 | + | - id: g_target_extract_speed | |
| 165 | + | type: int | |
| 166 | + | initial_value: "30" | |
| 167 | + | - id: g_target_bypass_temp | |
| 168 | + | type: int | |
| 169 | + | initial_value: "22" | |
| 170 | + | - id: g_target_bypass_mode | |
| 171 | + | type: int | |
| 172 | + | initial_value: "0" # 0: Auto, 1: Open, 2: Closed | |
| 173 | + | - id: g_unit_on | |
| 174 | + | type: bool | |
| 175 | + | initial_value: "true" | |
| 176 | + | - id: g_constant_flow | |
| 177 | + | type: bool | |
| 178 | + | initial_value: "true" | |
| 179 | + | - id: g_max_volume | |
| 180 | + | type: int | |
| 181 | + | initial_value: "${default_max_volume}" | |
| 182 | + | - id: g_vent_mode | |
| 183 | + | type: int | |
| 184 | + | initial_value: "0" # 0: Normal, 1: Fireplace, 2: Window, 3: Cooker Hood | |
| 185 | + | ||
| 186 | + | # ============================================================================== | |
| 187 | + | # CONTROLS — SWITCHES | |
| 188 | + | # ============================================================================== | |
| 189 | + | switch: | |
| 190 | + | # Ventilation unit master power (Unit State ON/OFF) | |
| 191 | + | - platform: template | |
| 192 | + | name: "Unit Power" | |
| 193 | + | id: sw_unit_power | |
| 194 | + | icon: "mdi:power" | |
| 195 | + | optimistic: true | |
| 196 | + | restore_mode: RESTORE_DEFAULT_ON | |
| 197 | + | turn_on_action: | |
| 198 | + | - lambda: 'id(g_unit_on) = true;' | |
| 199 | + | turn_off_action: | |
| 200 | + | - lambda: 'id(g_unit_on) = false;' | |
| 201 | + | ||
| 202 | + | # Constant Flow automatic compensation switch | |
| 203 | + | - platform: template | |
| 204 | + | name: "Constant Flow" | |
| 205 | + | id: sw_constant_flow | |
| 206 | + | icon: "mdi:gauge" | |
| 207 | + | optimistic: true | |
| 208 | + | restore_mode: RESTORE_DEFAULT_ON | |
| 209 | + | turn_on_action: | |
| 210 | + | - lambda: 'id(g_constant_flow) = true;' | |
| 211 | + | turn_off_action: | |
| 212 | + | - lambda: 'id(g_constant_flow) = false;' | |
| 213 | + | ||
| 214 | + | # ============================================================================== | |
| 215 | + | # CONTROLS — NUMERICAL INPUTS (NUMBER) | |
| 216 | + | # ============================================================================== | |
| 217 | + | number: | |
| 218 | + | # Target supply fan speed (0 - 100%) | |
| 219 | + | - platform: template | |
| 220 | + | name: "Target Supply Fan Speed" | |
| 221 | + | id: num_supply_speed | |
| 222 | + | unit_of_measurement: "%" | |
| 223 | + | icon: "mdi:fan" | |
| 224 | + | min_value: 0 | |
| 225 | + | max_value: 100 | |
| 226 | + | step: 5 | |
| 227 | + | mode: slider | |
| 228 | + | optimistic: true | |
| 229 | + | restore_value: true | |
| 230 | + | initial_value: 30 | |
| 231 | + | set_action: | |
| 232 | + | - lambda: 'id(g_target_supply_speed) = (int)x;' | |
| 233 | + | ||
| 234 | + | # Target extract fan speed (0 - 100%) | |
| 235 | + | - platform: template | |
| 236 | + | name: "Target Extract Fan Speed" | |
| 237 | + | id: num_extract_speed | |
| 238 | + | unit_of_measurement: "%" | |
| 239 | + | icon: "mdi:fan" | |
| 240 | + | min_value: 0 | |
| 241 | + | max_value: 100 | |
| 242 | + | step: 5 | |
| 243 | + | mode: slider | |
| 244 | + | optimistic: true | |
| 245 | + | restore_value: true | |
| 246 | + | initial_value: 30 | |
| 247 | + | set_action: | |
| 248 | + | - lambda: 'id(g_target_extract_speed) = (int)x;' | |
| 249 | + | ||
| 250 | + | # Bypass activation threshold temperature (18 - 26 °C) | |
| 251 | + | - platform: template | |
| 252 | + | name: "Bypass Threshold Temperature" | |
| 253 | + | id: num_bypass_temp | |
| 254 | + | unit_of_measurement: "°C" | |
| 255 | + | icon: "mdi:thermometer-chevron-up" | |
| 256 | + | min_value: 18 | |
| 257 | + | max_value: 26 | |
| 258 | + | step: 1 | |
| 259 | + | mode: box | |
| 260 | + | optimistic: true | |
| 261 | + | restore_value: true | |
| 262 | + | initial_value: 22 | |
| 263 | + | set_action: | |
| 264 | + | - lambda: 'id(g_target_bypass_temp) = (int)x;' | |
| 265 | + | ||
| 266 | + | # Maximum rated volume (x10 m³/h, e.g. 23 = 230 m³/h, 30 = 300 m³/h) | |
| 267 | + | - platform: template | |
| 268 | + | name: "Maximum Rated Volume" | |
| 269 | + | unit_of_measurement: "x10 m³/h" | |
| 270 | + | id: num_max_volume | |
| 271 | + | icon: "mdi:gauge-full" | |
| 272 | + | min_value: 15 | |
| 273 | + | max_value: 50 | |
| 274 | + | step: 1 | |
| 275 | + | mode: box | |
| 276 | + | optimistic: true | |
| 277 | + | restore_value: true | |
| 278 | + | initial_value: ${default_max_volume} | |
| 279 | + | set_action: | |
| 280 | + | - lambda: 'id(g_max_volume) = (int)x;' | |
| 281 | + | ||
| 282 | + | # ============================================================================== | |
| 283 | + | # CONTROLS — DROPDOWN MENUS (SELECT) | |
| 284 | + | # ============================================================================== | |
| 285 | + | select: | |
| 286 | + | # Bypass damper control mode | |
| 287 | + | - platform: template | |
| 288 | + | name: "Bypass Mode" | |
| 289 | + | id: sel_bypass_mode | |
| 290 | + | icon: "mdi:valve" | |
| 291 | + | optimistic: true | |
| 292 | + | restore_value: true | |
| 293 | + | initial_option: "Auto" | |
| 294 | + | options: | |
| 295 | + | - "Auto" | |
| 296 | + | - "Always Open" | |
| 297 | + | - "Always Closed" | |
| 298 | + | set_action: | |
| 299 | + | - lambda: |- | |
| 300 | + | if (x == "Auto") id(g_target_bypass_mode) = 0; | |
| 301 | + | else if (x == "Always Open") id(g_target_bypass_mode) = 1; | |
| 302 | + | else if (x == "Always Closed") id(g_target_bypass_mode) = 2; | |
| 303 | + | ||
| 304 | + | # Special ventilation mode | |
| 305 | + | - platform: template | |
| 306 | + | name: "Special Ventilation Mode" | |
| 307 | + | id: sel_vent_mode | |
| 308 | + | icon: "mdi:tune" | |
| 309 | + | optimistic: true | |
| 310 | + | restore_value: true | |
| 311 | + | initial_option: "Normal" | |
| 312 | + | options: | |
| 313 | + | - "Normal" | |
| 314 | + | - "Fireplace (Overpressure)" | |
| 315 | + | - "Open Window (Exhaust Only)" | |
| 316 | + | - "Cooker Hood (Compensation)" | |
| 317 | + | set_action: | |
| 318 | + | - lambda: |- | |
| 319 | + | if (x == "Normal") id(g_vent_mode) = 0; | |
| 320 | + | else if (x == "Fireplace (Overpressure)") id(g_vent_mode) = 1; | |
| 321 | + | else if (x == "Open Window (Exhaust Only)") id(g_vent_mode) = 2; | |
| 322 | + | else if (x == "Cooker Hood (Compensation)") id(g_vent_mode) = 3; | |
| 323 | + | ||
| 324 | + | # ============================================================================== | |
| 325 | + | # MAIN RS-485 COMMUNICATION LOOP (100% PURE YAML, ZERO EXTERNAL FILES) | |
| 326 | + | # ============================================================================== | |
| 327 | + | interval: | |
| 328 | + | - interval: 15ms | |
| 329 | + | then: | |
| 330 | + | - lambda: |- | |
| 331 | + | static uint8_t rx_buf[64]; | |
| 332 | + | static size_t rx_len = 0; | |
| 333 | + | static uint32_t last_rx_byte_ms = 0; | |
| 334 | + | static uint32_t last_status_ms = 0; | |
| 335 | + | static bool send_pending = false; | |
| 336 | + | ||
| 337 | + | // 1. Ingest incoming bytes from hardware UART FIFO into local buffer | |
| 338 | + | while (id(uart_bus)->available()) { | |
| 339 | + | uint8_t b; | |
| 340 | + | id(uart_bus)->read_byte(&b); | |
| 341 | + | if (rx_len < sizeof(rx_buf)) { | |
| 342 | + | rx_buf[rx_len++] = b; | |
| 343 | + | } | |
| 344 | + | last_rx_byte_ms = esphome::millis(); | |
| 345 | + | } | |
| 346 | + | ||
| 347 | + | // 2. Inter-frame silence timeout: if > 50 ms elapsed since last byte and incomplete frame, discard noise | |
| 348 | + | if (rx_len > 0 && (esphome::millis() - last_rx_byte_ms > 50) && rx_len < 22) { | |
| 349 | + | rx_len = 0; | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | // 3. Sliding Window parser for 0x63 status frame | |
| 353 | + | while (rx_len >= 22) { | |
| 354 | + | // Seek start byte 0x63 | |
| 355 | + | if (rx_buf[0] != 0x63) { | |
| 356 | + | memmove(&rx_buf[0], &rx_buf[1], --rx_len); | |
| 357 | + | continue; | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | int8_t t_outdoor = (int8_t)rx_buf[1]; | |
| 361 | + | int8_t t_exhaust = (int8_t)rx_buf[2]; | |
| 362 | + | int8_t t_supply = (int8_t)rx_buf[3]; | |
| 363 | + | int8_t t_extract = (int8_t)rx_buf[4]; | |
| 364 | + | uint8_t state = rx_buf[5]; | |
| 365 | + | uint8_t bypass = rx_buf[8]; | |
| 366 | + | uint8_t sup_vol = rx_buf[17]; | |
| 367 | + | uint8_t ext_vol = rx_buf[18]; | |
| 368 | + | ||
| 369 | + | // Robust multi-level sanity validation (compensating lack of CRC checksum) | |
| 370 | + | bool valid = (rx_buf[6] == 0 && rx_buf[7] == 0) && | |
| 371 | + | (rx_buf[20] == 0 && rx_buf[21] == 0) && | |
| 372 | + | (state == 0 || state == 5 || state == 10) && | |
| 373 | + | (bypass == 0 || bypass == 1) && | |
| 374 | + | (t_outdoor >= -35 && t_outdoor <= 55) && | |
| 375 | + | (t_exhaust >= -35 && t_exhaust <= 55) && | |
| 376 | + | (t_supply >= -35 && t_supply <= 55) && | |
| 377 | + | (t_extract >= -35 && t_extract <= 55) && | |
| 378 | + | (sup_vol <= 60 && ext_vol <= 60); | |
| 379 | + | ||
| 380 | + | if (valid) { | |
| 381 | + | // Publish temperatures (signed int8_t for sub-zero support) | |
| 382 | + | id(s_outdoor_temp).publish_state(t_outdoor); | |
| 383 | + | id(s_exhaust_temp).publish_state(t_exhaust); | |
| 384 | + | id(s_supply_temp).publish_state(t_supply); | |
| 385 | + | id(s_extract_temp).publish_state(t_extract); | |
| 386 | + | ||
| 387 | + | // Calculate recovery efficiency | |
| 388 | + | if (t_extract != t_outdoor) { | |
| 389 | + | float eff = ((float)(t_supply - t_outdoor) / (float)(t_extract - t_outdoor)) * 100.0f; | |
| 390 | + | if (eff < 0.0f) eff = 0.0f; | |
| 391 | + | if (eff > 100.0f) eff = 100.0f; | |
| 392 | + | id(s_efficiency).publish_state(eff); | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | // Publish air flow volume in m³/h (raw byte * 10) | |
| 396 | + | id(s_supply_volume).publish_state(sup_vol * 10); | |
| 397 | + | id(s_extract_volume).publish_state(ext_vol * 10); | |
| 398 | + | ||
| 399 | + | // Publish binary sensors (Bypass, Cover, Defrost) | |
| 400 | + | id(bs_bypass_state).publish_state(bypass == 1); | |
| 401 | + | id(bs_cover_open).publish_state(state == 5); | |
| 402 | + | id(bs_defrost_active).publish_state(state == 10); | |
| 403 | + | ||
| 404 | + | ESP_LOGI("IZZIFast", "Status 0x63: Out=%d°C, Exh=%d°C, Sup=%d°C, Ext=%d°C | Flow=%d/%d m³/h | Bypass=%s | State=%d", | |
| 405 | + | t_outdoor, t_exhaust, t_supply, t_extract, sup_vol * 10, ext_vol * 10, bypass ? "OPEN" : "CLOSED", state); | |
| 406 | + | ||
| 407 | + | last_status_ms = esphome::millis(); | |
| 408 | + | send_pending = true; | |
| 409 | + | ||
| 410 | + | // Consume valid 22-byte frame from buffer | |
| 411 | + | memmove(&rx_buf[0], &rx_buf[22], rx_len - 22); | |
| 412 | + | rx_len -= 22; | |
| 413 | + | } else { | |
| 414 | + | // Corrupted packet or false 0x63 byte: slide by only 1 byte to keep synchronization | |
| 415 | + | memmove(&rx_buf[0], &rx_buf[1], --rx_len); | |
| 416 | + | ESP_LOGW("IZZIFast", "Corrupted 0x63 header or sanity check failed, sliding window 1 byte"); | |
| 417 | + | } | |
| 418 | + | } | |
| 419 | + | ||
| 420 | + | // 2. Transmit 0x64 command frame (after min. 15 ms turnaround from status frame) | |
| 421 | + | if (send_pending && (esphome::millis() - last_status_ms >= 15)) { | |
| 422 | + | send_pending = false; | |
| 423 | + | ||
| 424 | + | uint8_t actual_supply = (uint8_t)id(g_target_supply_speed); | |
| 425 | + | uint8_t actual_extract = (uint8_t)id(g_target_extract_speed); | |
| 426 | + | ||
| 427 | + | // Apply special ventilation mode modifiers | |
| 428 | + | switch (id(g_vent_mode)) { | |
| 429 | + | case 1: // Fireplace (exhaust -20% to create slight overpressure) | |
| 430 | + | actual_extract = (uint8_t)(actual_extract * 0.80f); | |
| 431 | + | break; | |
| 432 | + | case 2: // Open window (exhaust only, supply 0%) | |
| 433 | + | actual_supply = 0; | |
| 434 | + | break; | |
| 435 | + | case 3: // Cooker hood (exhaust -70% to compensate kitchen extraction) | |
| 436 | + | actual_extract = (uint8_t)(actual_extract * 0.30f); | |
| 437 | + | break; | |
| 438 | + | default: | |
| 439 | + | break; | |
| 440 | + | } | |
| 441 | + | ||
| 442 | + | // EC motor minimum speed protection when unit is powered on | |
| 443 | + | if (id(g_unit_on)) { | |
| 444 | + | if (actual_supply > 0 && actual_supply < 15) actual_supply = 15; | |
| 445 | + | if (actual_extract > 0 && actual_extract < 15) actual_extract = 15; | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | // Construct 21-byte command frame 0x64 | |
| 449 | + | uint8_t cmd[21] = { | |
| 450 | + | 0x64, | |
| 451 | + | 0x00, 0x00, 0x00, 0x00, 0x16, 0x05, 0x00, | |
| 452 | + | (uint8_t)id(g_target_bypass_temp), | |
| 453 | + | (uint8_t)id(g_target_bypass_mode), | |
| 454 | + | actual_supply, | |
| 455 | + | actual_extract, | |
| 456 | + | (uint8_t)(id(g_unit_on) ? 0 : 1), | |
| 457 | + | (uint8_t)id(g_max_volume), | |
| 458 | + | (uint8_t)(id(g_constant_flow) ? 1 : 0), | |
| 459 | + | 0x00, 0x02, 0x00, 0x00, 0x00, 0x00 | |
| 460 | + | }; | |
| 461 | + | ||
| 462 | + | // Send 21-byte command frame 0x64. | |
| 463 | + | // ESP32 hardware UART in flow_control_pin mode automatically asserts GPIO 27 (DE/RE) | |
| 464 | + | // and de-asserts it with sub-microsecond precision immediately after the last stop bit! | |
| 465 | + | id(uart_bus)->write_array(cmd, 21); | |
| 466 | + | ||
| 467 | + | ESP_LOGI("IZZIFast", "Command 0x64 sent: Sup=%d%%, Ext=%d%%, Unit=%s, BypassMode=%d, MaxVol=%d, CF=%s", | |
| 468 | + | actual_supply, actual_extract, id(g_unit_on) ? "ON" : "OFF", id(g_target_bypass_mode), id(g_max_volume), id(g_constant_flow) ? "ON" : "OFF"); | |
| 469 | + | } | |
Newer
Older