Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Note about CMD 0x05 observed with payload 0x00. Added to PROTOCOL.md and new sample trace.
- Note about CMD 0x12 observed with payload 0x01 0x00. Added to PROTOCOL.md and new sample trace.
- Parsing for CMD 0x12 updated so `0x01 0x00` no longer reported as unexpected.

- Full decoding of CMD 0x12 status byte for Gas Heaters (HiNRG `0x0072` and ICI (`0x0074`))
### Removed
### Fixed
### Deprecated
Expand Down
37 changes: 27 additions & 10 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,17 +463,17 @@ Data fields:

---

#### ICI Gas Heater (`0x0074`) ✅
#### Gas Heaters: HiNRG (`0x0072`) & ICI (`0x0074`) ✅

Pattern: `02 00 74 FF FF 80 00 12 10 16`

Examples:

```
02 00 74 FF FF 80 00 12 10 16 00 00 00 00 00 03 Idle / off
02 00 74 FF FF 80 00 12 10 16 00 01 00 00 01 03 On and Lighting
02 00 74 FF FF 80 00 12 10 16 00 01 00 00 01 03 Heater On, no water flow yet
02 00 74 FF FF 80 00 12 10 16 00 03 00 00 03 03 At Setpoint (on but not heating)
02 00 74 FF FF 80 00 12 10 16 00 07 00 00 07 03 (transitional?)
02 00 74 FF FF 80 00 12 10 16 00 07 00 00 07 03 Igniting
02 00 74 FF FF 80 00 12 10 16 00 0F 00 00 0F 03 Heater Lit and Running
^^ Status byte
```
Expand All @@ -486,13 +486,30 @@ Data fields:

Observed status values (payload[1]):

| Value | Meaning |
|--------|---------|
| `0x00` | Idle / off |
| `0x01` | On and Lighting (attempting ignition) |
| `0x03` | At Setpoint — on but not heating |
| `0x07` | Transitional? (observed briefly between `0x01` and `0x0F`) |
| `0x0F` | Heater Lit and Running |
| Value | Bits 7–5 Diagnostics | Bit 4 <br> Locked Out | Bit 3 <br> Flame | Bit 2 <br> Gas Valve | Bit 1 <br> Pressure / Flow | Bit 0 <br> Heater On | Meaning |
|--------|---------|-------|-------|-------|-------|-------|---------|
| `0x00` | X | 0 | 0 | 0 | 0 | 0 | System Idle (Heater Off, No Water Flow)|
| `0x01` | X | 0 | 0 | 0 | 0 | 1 | Heater On / No Flow (temporary state if heater is turned on while pump is off)|
| `0x02` | X | 0 | 0 | 0 | 1 | 0 | Heater Off / Water Flow (Normal state when heater is off and pump is running) |
| `0x03` | X | 0 | 0 | 0 | 1 | 1 | Setpoint Reached |
| `0x07` | X | 0 | 0 | 1 | 1 | 1 | Igniting |
| `0x0F` | X | 0 | 1 | 1 | 1 | 1 | Heating |
| `0x12` | X | 1 | 0 | 0 | 1 | 0 | Cooling Down (Heater Off, Pump Forced On)|
| `0x13` | X | 1 | 0 | 0 | 1 | 1 | Locked Out (Heater On, Pump Forced On) |

Some notes:

- 'Heater On' is required for 'Gas Valve' to open.
- 'Pressure / Flow' is required for 'Gas Valve' to open.
- 'Gas Valve' is required for 'Flame'.
- 'Pressure / Flow' is required for 'Locked Out'.
- 'Locked Out' implies 'Gas Valve' is closed.

Diagnostic Bits
- Bit 5: General Service Required
- Bit 6: Ignition Service Required
- Bit 7: Cooling Available (Heatpump installed?)
It is unclear whether the diagnostic bits can be set at the same time as the functional status bits.

Payload[1] is the only byte that varies; bytes 10, 12, and 13 are always `0x00`. The data checksum (byte 14) equals payload[1] since all other payload bytes are zero.

Expand Down
2 changes: 1 addition & 1 deletion dependencies.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies:
idf:
source:
type: idf
version: 5.5.1
version: 5.5.4
direct_dependencies:
- espressif/led_strip
- espressif/mdns
Expand Down
126 changes: 123 additions & 3 deletions main/message_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ static const channel_type_entry_t CHANNEL_TYPE_TABLE[] = {

#define CHANNEL_TYPE_TABLE_SIZE (sizeof(CHANNEL_TYPE_TABLE) / sizeof(CHANNEL_TYPE_TABLE[0]))

// Gas Heater Status Field Bitmasks
static const int GAS_HEATER_BITMASK_HEATER_ON = 0x01;
static const int GAS_HEATER_BITMASK_WATER_FLOW = 0x02;
static const int GAS_HEATER_BITMASK_GAS_VALVE = 0x04;
static const int GAS_HEATER_BITMASK_BURNER_ALIGHT = 0x08;
static const int GAS_HEATER_BITMASK_LOCKED_OUT = 0x10;
static const int GAS_HEATER_BITMASK_GENERAL_SERVICE_REQUIRED = 0x20;
static const int GAS_HEATER_BITMASK_IGNITION_SERVICE_REQUIRED = 0x40;
static const int GAS_HEATER_BITMASK_COOLING_AVAILABLE = 0x80;
// The lower 5 bits are for general heater functions
static const int GAS_HEATER_BITMASK_FUNCTIONAL_STATUS = 0x1F;

/**
* Get channel type name from type code
* @param type_code Channel type code (0x00-0x12, 0xFD, 0xFE)
Expand Down Expand Up @@ -267,6 +279,24 @@ const char *LIGHT_ZONE_NAME_TABLE[] = {
"Waterfall 3", // 0x05
};

// Gas heater status names (indexed by gas_heater_status_t)
const char *HEATER_STATUS_NAMES[] = {
"Off", // HEATER_OFF
"No Flow", // HEATER_ON_NO_FLOW
"Igniting", // HEATER_IGNITING
"Heating", // HEATER_HEATING
"Setpoint Reached", // HEATER_SETPOINT_REACHED
"Cooldown", // HEATER_COOLDOWN
"Locked Out", // HEATER_LOCKED_OUT
};

// Gas heater burner state names (indexed by gas_heater_burner_state_t)
const char *BURNER_STATE_NAMES[] = {
"Off", // BURNER_OFF
"Igniting", // BURNER_IGNITING
"Alight", // BURNER_ALIGHT
};

// Day of week names
const char *DAY_OF_WEEK_NAMES[] = {
"Monday", // 0
Expand Down Expand Up @@ -897,8 +927,98 @@ static bool handle_gas_heater_status(
message_decoder_context_t *ctx)
{
if (payload_len < 4) return false;
ESP_LOGI(TAG, "%s Gas heater status - [%02X %02X %02X %02X]",
addr_info, payload[0], payload[1], payload[2], payload[3]);


const uint8_t raw_status = payload[1];
const uint8_t functional_status = raw_status & GAS_HEATER_BITMASK_FUNCTIONAL_STATUS;
gas_heater_status_t heater_status;

switch (functional_status) {
case 0x00:
heater_status = HEATER_OFF;
break;
case 0x01:
heater_status = HEATER_ON_NO_FLOW;
break;
case 0x02:
heater_status = HEATER_OFF;
break;
case 0x03:
heater_status = HEATER_SETPOINT_REACHED;
break;
case 0x07:
heater_status = HEATER_IGNITING;
break;
case 0x0F:
heater_status = HEATER_HEATING;
break;
case 0x12:
heater_status = HEATER_COOLDOWN;
break;
case 0x13:
heater_status = HEATER_LOCKED_OUT;
break;
default:
ESP_LOGW(TAG, "Invalid gas heater status:%d", functional_status);
return false;
}

const bool heater_on = (raw_status & GAS_HEATER_BITMASK_HEATER_ON) != 0;
const bool water_flow_detected = (raw_status & GAS_HEATER_BITMASK_WATER_FLOW) != 0;
const bool gas_valve_open = (raw_status & GAS_HEATER_BITMASK_GAS_VALVE) != 0;
const bool burner_alight = (raw_status & GAS_HEATER_BITMASK_BURNER_ALIGHT) != 0;
const bool locked_out = (raw_status & GAS_HEATER_BITMASK_LOCKED_OUT) != 0;
const bool general_service_required = (raw_status & GAS_HEATER_BITMASK_GENERAL_SERVICE_REQUIRED) != 0;
const bool ignition_service_required = (raw_status & GAS_HEATER_BITMASK_IGNITION_SERVICE_REQUIRED) != 0;
const bool cooling_available = (raw_status & GAS_HEATER_BITMASK_COOLING_AVAILABLE) != 0;

gas_heater_burner_state_t burner_state;
if (!gas_valve_open && !burner_alight) {
burner_state = BURNER_OFF;
} else if (gas_valve_open && !burner_alight) {
burner_state = BURNER_IGNITING;
} else if (gas_valve_open && burner_alight) {
burner_state = BURNER_ALIGHT;
} else {
// This should never happen given the valid status filter above
ESP_LOGW(TAG, "Invalid gas burner state: gas_valve=%d, alight=%d", gas_valve_open, burner_alight);
return false;
}

ESP_LOGI(TAG, "%s Gas heater raw status - [%02X %02X %02X %02X], decoded status - \"%s\"",
addr_info, payload[0], payload[1], payload[2], payload[3], HEATER_STATUS_NAMES[heater_status]);
if (general_service_required || ignition_service_required) {
ESP_LOGI(TAG, "%s Gas heater service required - general %s, ignition %s",
addr_info, general_service_required ? "true" : "false", ignition_service_required ? "true" : "false");
}

// Update state and publish
pool_state_t snapshot;
if (!ctx->state_mutex || xSemaphoreTake(ctx->state_mutex, pdMS_TO_TICKS(MUTEX_TIMEOUT_MS)) != pdTRUE) {
ESP_LOGW(TAG, "Failed to acquire mutex for heater");
return true;
}
pool_heater_t* const heater_state = &(ctx->pool_state->heaters[0]);
heater_state->valid = true;
heater_state->on = heater_on;

heater_state->gas_heater_valid = true;
heater_state->water_flow_detected = water_flow_detected;
heater_state->locked_out = locked_out;
heater_state->burner_state = burner_state;
heater_state->general_service_required = general_service_required;
heater_state->ignition_service_required = ignition_service_required;
heater_state->cooling_available = cooling_available;
heater_state->status = heater_status;

ctx->pool_state->last_update_ms = xTaskGetTickCount() * portTICK_PERIOD_MS;
snapshot = *ctx->pool_state;
xSemaphoreGive(ctx->state_mutex);

if (ctx->enable_mqtt) {
mqtt_publish_gas_heater(&snapshot, 0);
}

return true;
}

Expand Down Expand Up @@ -3008,7 +3128,7 @@ static bool dispatch_message(
return handle_heater(data, len, payload, payload_len, addr_info, ctx);
}

// Genus Heater (0x0070) messages
// Heater temperature setting messages
if (match_pattern(data, len, MSG_TYPE_GENUS_HEATER_TEMP_SETTING) ||
match_pattern(data, len, MSG_TYPE_HINRG_HEATER_TEMP_SETTING) ||
match_pattern(data, len, MSG_TYPE_ICI_HEATER_TEMP_SETTING)) {
Expand Down
Loading