21constexpr uint16_t LOW_BYTE_MASK = 0xFF;
24constexpr float TENTHS_PER_DEGREE = 10.0F;
27constexpr int8_t NO_PRESET = -1;
31void preset_missing_from_list() {}
34constexpr bool same_name(
const char *a,
const char *b) {
35 if (a ==
nullptr || b ==
nullptr)
37 while (*a !=
'\0' && *a == *b) {
50consteval int8_t preset_index(std::span<const HeatingPresetEntry> presets,
HeatingPresetId id,
51 const char *custom_name =
nullptr) {
52 for (
size_t i = 0; i < presets.size(); i++) {
53 if (presets[i].
id ==
id && same_name(presets[i].custom_name, custom_name))
54 return static_cast<int8_t
>(i);
56 preset_missing_from_list();
123constexpr uint8_t TOWEL_DRYER_NO_WIRE = 0;
278constexpr float TEMPERATURE_VISUAL_MIN_C = 5.0F;
279constexpr float TEMPERATURE_VISUAL_MAX_C = 30.0F;
288 .features = {.target_temperature =
true, .current_temperature =
true, .action =
true, .mode_observed =
true},
289 .modes = SETPOINT_HEATER_MODES,
292 .presets = SETPOINT_HEATER_PRESETS,
293 .observed = SETPOINT_HEATER_OBSERVED,
294 .poll_registers = SETPOINT_HEATER_POLL,
295 .boot_registers = SETPOINT_HEATER_BOOT,
304 profile.extra_poll_registers = HEATER_WITH_LIGHT_EXTRA_POLL;
309constexpr HeatingProfile HEATER_WITH_LIGHT_PROFILE = heater_with_light_profile();
317 .features = {.target_temperature =
true, .current_temperature =
false, .action =
false, .mode_observed =
false},
318 .modes = TEMPERATURE_INTERFACE_MODES,
319 .setpoint_fp = THERMOSTAT_MANUAL,
320 .visual_min_c = TEMPERATURE_VISUAL_MIN_C,
321 .visual_max_c = TEMPERATURE_VISUAL_MAX_C,
322 .presets = TEMPERATURE_INTERFACE_PRESETS,
323 .observed = TEMPERATURE_INTERFACE_OBSERVED,
324 .mp_prefix = HEATING_ALIAS_MP_PREFIX,
331 .features = {.target_temperature =
true, .current_temperature =
false, .action =
false, .mode_observed =
false},
332 .modes = HEAT_PUMP_MODES,
333 .visual_min_c = TEMPERATURE_VISUAL_MIN_C,
334 .visual_max_c = TEMPERATURE_VISUAL_MAX_C,
335 .presets = HEAT_PUMP_PRESETS,
336 .observed = HEAT_PUMP_OBSERVED,
337 .mp_prefix = HEATING_ALIAS_MP_PREFIX,
345 .features = {.target_temperature =
false, .current_temperature =
false, .action =
false, .mode_observed =
true},
346 .modes = LEVEL_HEATER_MODES,
347 .presets = LEVEL_HEATER_PRESETS,
348 .observed = LEVEL_HEATER_OBSERVED,
349 .mp_prefix = HEATING_LEVEL_MP_PREFIX,
351 SETPOINT_HEATER_PROFILE,
352 HEATER_WITH_LIGHT_PROFILE,
358 .features = {.target_temperature =
true, .current_temperature =
true, .action =
true, .mode_observed =
true},
359 .modes = TOWEL_DRYER_MODES,
362 .programme_period = &TOWEL_DRYER_PROGRAMME_PERIOD,
363 .presets = TOWEL_DRYER_PRESETS,
364 .observed = TOWEL_DRYER_OBSERVED,
365 .poll_registers = TOWEL_DRYER_POLL,
366 .boot_registers = TOWEL_DRYER_BOOT,
367 .preset_observed = TOWEL_DRYER_OBSERVED_PRESETS,
377constexpr bool observed_within(std::span<const HeatingObservedState> rows,
size_t preset_count) {
379 return row.preset >= NO_PRESET && row.preset <
static_cast<int8_t
>(preset_count);
384constexpr bool programme_period_within(
const HeatingProfile &profile) {
386 const auto count =
static_cast<int8_t
>(profile.presets.size());
387 return period ==
nullptr || (period->comfort_preset < count && period->eco_preset < count);
391constexpr bool preset_indices_within_lists() {
392 return std::ranges::all_of(HEATING_PROFILES, [](
const HeatingProfile &profile) {
393 return observed_within(profile.observed, profile.presets.size()) &&
394 observed_within(profile.preset_observed, profile.presets.size()) && programme_period_within(profile);
398static_assert(preset_indices_within_lists(),
"a profile names a preset index outside its preset list");
402 if (profile.type == type && profile.subtype == subtype && profile.model == model)
412 if (
const HeatingProfile *model_profile = find_exact_profile(type, subtype, model))
413 return model_profile;
416 if (profile !=
nullptr || subtype == 0)
452 if (!std::isfinite(celsius) || !std::isfinite(min_c) || !std::isfinite(max_c) || !(max_c > min_c))
454 const float steps = std::round((celsius - min_c) / (max_c - min_c) *
static_cast<float>(HEATING_TEMPERATURE_MP_SPAN));
455 main_parameter =
static_cast<uint16_t
>(std::clamp(steps, 0.0F,
static_cast<float>(HEATING_TEMPERATURE_MP_SPAN)));
460 if (main_parameter > HEATING_TEMPERATURE_MP_SPAN || !std::isfinite(min_c) || !std::isfinite(max_c) ||
464 min_c + (
static_cast<float>(main_parameter) /
static_cast<float>(HEATING_TEMPERATURE_MP_SPAN) * (max_c - min_c));
469 if ((main_parameter >> BITS_PER_BYTE) != HEATING_LEVEL_MP_PREFIX)
471 level =
static_cast<uint8_t
>(main_parameter & LOW_BYTE_MASK);
478const HeatingObservedState *find_observed_row(std::span<const HeatingObservedState> rows, uint8_t wire) {
479 for (
const HeatingObservedState &row : rows) {
480 if (row.wire == wire)
489 return find_observed_row(profile.
observed, wire);
500 if (period ==
nullptr)
504 return readings.
value_of(fn) ==
static_cast<int16_t
>(value);
512int32_t to_tenths(
float celsius) {
return static_cast<int32_t
>(std::lround(celsius * TENTHS_PER_DEGREE)); }
529 const int32_t in_force = to_tenths(state.
target_c);
531 if (in_force == comfort) {
void clear_inactive_programme_reading(const HeatingProfile &profile, HeatingReadings &readings)
Drop the temperature in force from the readings when the programme does not run.
@ WORKING_RATE
The working rate (HeatingFunction::WORKING_RATE): heating above 0 %.
const HeatingObservedState * find_observed_state(const HeatingProfile &profile, uint8_t wire)
Find what a device-reported wire value shows on the card.
DeviceFeatures device_features(DeviceType type, uint8_t subtype, DeviceModel model)
Capability bits of a (type, subtype, model).
bool encode_temperature_main_parameter(float celsius, float min_c, float max_c, uint16_t &main_parameter)
Encode a temperature as a main parameter over an encoding range.
@ FP9
Functional parameter 9, one byte.
@ FP16
Functional parameter 16, two bytes.
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the heating profile for a (type, subtype, model).
HeatingFunction
Registers and functions of a 2W heater, one row of the descriptor table each.
@ TOWEL_MODE
Register 0x0136, read/write: a towel dryer's mode (TowelDryerMode).
@ ROOM_TEMPERATURE
Register 0x0115, read-only: measured room temperature, signed hundredths of a degree.
@ WORKING_RATE
Register 0x0126, read-only: share of the time the element heats, 0-100 %.
@ NIGHT_LIGHT
Register 0x0144, read/write: a heater's night light on (1) or off (0).
@ MAX_SETPOINT
Register 0x0102, read-only: highest setpoint the heater accepts, tenths of a degree.
@ TEMPORARY_STATE
Register 0x013E, read/write: a towel dryer's temporary state (HeatingTemporaryState).
@ NIGHT_LIGHT_BRIGHTNESS
Register 0x0142, read/write: the night light's brightness, 0-100 %, 16-bit.
@ REGULATOR_STATE
Register 0x0117, read-only: 0 none, 1 standby, 2 heating.
@ WINDOW_OPEN
Register 0x010E, read-only: the heater's window sensor, 0 closed / 1 open.
@ SET_OPEN_WINDOW_DETECTION
Register 0x0106, read/write: open-window detection on (1) or off (0).
@ SET_TEMPERATURE
Register 0x0103, read/write: comfort setpoint, tenths of a degree.
@ TEMPERATURE_IN_FORCE
Register 0x0137, read-only: the temperature a towel dryer holds right now, tenths of a degree.
@ ECO_SETBACK
Register 0x0104, read-only: how far below the comfort setpoint a towel dryer's eco period heats,...
@ SET_MODE
Register 0x0100, read/write: operating mode (HeatingMode).
bool decode_temperature_main_parameter(uint16_t main_parameter, float min_c, float max_c, float &celsius)
Decode a main parameter as a temperature over an encoding range (the inverse of encode_temperature_ma...
@ PROG
Program mode: the device runs its own weekly program.
@ MANUAL
Manual mode: the device holds the comfort setpoint.
HeatingPresetId
Presets a profile can offer. CUSTOM entries carry their own name.
@ CUSTOM
Named by HeatingPresetEntry::custom_name.
@ AWAY
Nobody is in the room (the frost-protection level on a level heater).
@ NONE
No preset: selecting it ends the one that runs (a towel dryer's boost or drying).
DeviceModel
A product model below a (node type, subtype): for products that report the same wire type but need di...
@ HEATER_WITH_LIGHT
Heater with a night light under the electrical heater with an adjustable setpoint (52/1).
@ NONE
No model: the (type, subtype) row.
@ TOWEL_DRYER
Towel dryer under the electrical heater with an adjustable setpoint (52/1).
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
bool programme_period_active(const HeatingProfile &profile, const HeatingReadings &readings)
Does the heater's own programme run right now, with no timed run on top of it?
DeviceType
IO-Homecontrol node types.
@ ELECTRICAL_HEATER
Electrical heater.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ TEMPERATURE_EXECUTE
Standard EXECUTE with a temperature or an alias as the main parameter; read with 0x03.
@ PRIVATE_REGISTERS
CMD_WRITE_PRIVATE register reads and writes (proto_heating.h).
@ HEATING_LEVEL
Standard EXECUTE with a heating level as the main parameter; read with 0x03.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
const HeatingObservedState * find_observed_preset(const HeatingProfile &profile, uint8_t wire)
Find what a device-reported value of the preset register (preset_fn) shows on the card.
const DeviceTypeInfo * find_exact_device_type_info(DeviceType type, uint8_t subtype)
Look up the table row with exactly this (type, subtype), without falling back.
constexpr float HEATING_TEMP_MIN_C
Lowest setpoint this codec will encode.
bool device_uses_heating_levels(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with heating levels?
bool decode_heating_level(uint16_t main_parameter, uint8_t &level)
Read a heating level out of a main parameter.
@ PROG
Program mode; the device runs its own stored weekly schedule.
@ EXTERNAL_SETPOINT
Setpoint comes from an external controller.
@ MANUAL
Manual mode; follows the last written setpoint.
@ AUTO
Automatic mode; the device manages the setpoint itself.
bool device_uses_heating_registers(DeviceType type, uint8_t subtype)
Does a climate entity drive this device over the private register protocol?
ProgrammePeriodState resolve_programme_period(const HeatingProfile &profile, const HeatingReadings &readings)
Which period of its programme a heater is in, and the temperature it heats to.
bool device_uses_heating_temperature(DeviceType type, uint8_t subtype)
Does a climate entity drive this device with a temperature or an alias in the main parameter?
bool device_accepts_register_probe(DeviceType type, uint8_t subtype)
May the diagnostic register-read probes (register_00, register_01) be sent to this device?
@ DRYING
Drying: full power for the drying duration.
@ BOOST
Boost: full power for the boost duration.
@ NONE
No temporary state: the mode applies.
constexpr uint16_t heating_prefixed_main_parameter(uint8_t prefix, uint8_t low)
Main parameter of a prefixed value (a level or an alias).
@ AUTO
The device decides.
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
Heating profiles: what a climate card offers for each kind of heater, and how.
Fundamental IO-Homecontrol frame and crypto size constants.
A functional parameter and its value.
One HVAC mode the card offers and the wire value that selects it.
What a device-reported wire value shows on the card.
One preset the card offers.
DeviceModel model
Model (NONE = the (type, subtype) row's own profile).
std::span< const HeatingObservedState > observed
Device-reported value -> card state.
const HeatingProgrammePeriod * programme_period
PRIVATE_REGISTERS: how the heater's own programme names the comfort or eco period it is in; nullptr f...
HeatingControlPath path
How the heater is commanded and read.
HeatingFunction preset_fn
PRIVATE_REGISTERS with preset_observed: the register the card's preset is read from.
HeatingFunction mode_fn
PRIVATE_REGISTERS: the register the HVAC modes write and the card's mode is read from.
std::span< const HeatingObservedState > preset_observed
Device-reported value of preset_fn -> preset; empty when the mode register names the preset too (obse...
How a heater's own weekly programme shows which period runs: the registers that tell,...
HeatingFunction in_force_fn
Register of the temperature in force.
uint8_t programme_mode
Value of HeatingProfile::mode_fn while the programme runs.
int8_t comfort_preset
Index into HeatingProfile::presets shown in a comfort period.
int8_t eco_preset
Index into HeatingProfile::presets shown in an eco period.
HeatingFunction setback_fn
Register of the eco setback, a distance below the comfort setpoint.
HeatingFunction comfort_fn
Register of the comfort setpoint.
uint8_t idle_preset_state
Value of HeatingProfile::preset_fn with no timed run on top.
What the hub has read from one heater so far.
int16_t value_of(HeatingFunction fn) const
The stored integer value of a register (a mode, a state, a flag, a percentage).
bool has(HeatingFunction fn) const
Has this register been read yet?
bool clear(HeatingFunction fn)
Forget a register's value: it reads as "never read" again.
float temperature_of(HeatingFunction fn) const
The stored temperature of a register read in degrees.
Runtime state of a paired IO‑Homecontrol device.
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceModel model
YAML-declared product model below the subtype (io_model:); never on the wire.
DeviceType type
Device type (shutter, awning, etc.).
What a heater's programme period resolves to, from its readings.
int8_t preset_index
Index into the profile's presets of the period, or -1 when the temperature in force fits neither peri...
bool active
The programme runs (and no timed run is on top of it).
float target_c
The temperature in force; NaN until it has been read.