diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b105cb49..0ecf8d062 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Support for GUI tooltip on touch media like phones and tablets - ESP32 MiEL HVAC Modbus RTU slave on a second RS485 port exposing all states and functions for PLC use with `#define USE_MIEL_HVAC_MODBUS_SLAVE` (#24982) +- MiEL HVAC climate control panel on the web UI main page (mode, target temperature, fan, vanes, air direction) with live state (xdrv_44) - Support for TFA Dostmann Marbella 868MHz pool thermometer using a CC1101 (#24959) ### Breaking Changed diff --git a/tasmota/tasmota_xdrv_driver/xdrv_44_miel_hvac.ino b/tasmota/tasmota_xdrv_driver/xdrv_44_miel_hvac.ino index 3c47193e4..ce9cafc95 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_44_miel_hvac.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_44_miel_hvac.ino @@ -3566,31 +3566,640 @@ miel_hvac_sensor(struct miel_hvac_softc *sc) #ifdef USE_WEBSERVER /* - * Web UI sensor display — shows instantaneous Power (W) and cumulative - * Total energy (kWh) rows on the Tasmota main page, matching how other - * energy drivers render their values. + * Current mode as a control-panel keyword ("off" when powered down; i-See + * mode variants folded onto their base mode). + */ +static const char * +miel_hvac_web_curmode(const struct miel_hvac_data_settings *set) +{ + if (!set->power) + return ("off"); + + switch (set->mode & MIEL_HVAC_SETTINGS_MODE_MASK) + { + case MIEL_HVAC_SETTINGS_MODE_HEAT: + case MIEL_HVAC_SETTINGS_MODE_HEAT_ISEE: + return ("heat"); + case MIEL_HVAC_SETTINGS_MODE_COOL: + case MIEL_HVAC_SETTINGS_MODE_COOL_ISEE: + return ("cool"); + case MIEL_HVAC_SETTINGS_MODE_DRY: + case MIEL_HVAC_SETTINGS_MODE_DRY_ISEE: + return ("dry"); + case MIEL_HVAC_SETTINGS_MODE_FAN: + return ("fan"); + case MIEL_HVAC_SETTINGS_MODE_AUTO: + return ("auto"); + } + return (""); +} + +/* Apply the locale decimal separator to a plain number string in place. */ +static void +miel_hvac_dsep(char *s) +{ + if (D_DECIMAL_SEPARATOR[0] == '.') + return; + for (; *s != '\0'; s++) + { + if (*s == '.') + *s = D_DECIMAL_SEPARATOR[0]; + } +} + +/* + * Emit one state row. js=false renders the initial into the panel + * card (miel_hvac_web_panel); js=true emits a JS statement that refreshes + * that same row's in place, driven from FUNC_WEB_SENSOR so the values + * update without a page reload. + */ +static void +miel_hvac_web_ro(bool js, const char *id, const char *label, const char *value) +{ + char v[48]; + size_t i; + + /* protocol keywords use '_' between words; show them with spaces */ + for (i = 0; i + 1 < sizeof(v) && value[i] != '\0'; i++) + v[i] = (value[i] == '_') ? ' ' : value[i]; + v[i] = '\0'; + + if (js) + WSContentSend_P(PSTR("(e=eb('%s'))&&(e.innerHTML='%s');"), id, v); + else + WSContentSend_P(PSTR("%s%s"), + label, id, v); +} + +/* + * Read-only state table shown inside the control-panel card, below the + * controls (matching the design proposal). Called once with js=false to + * lay it out, then every Settings->web_refresh ms with js=true to refresh + * the values live. + */ +static void +miel_hvac_web_readout(struct miel_hvac_softc *sc, bool js) +{ + const struct miel_hvac_data_settings *set = &sc->sc_settings.data.settings; + uint8_t traw = (set->temp05 != 0) ? set->temp05 : set->temp; + const char *name; + const char *vh; + char vhbuf[8]; + char val[48]; + char buf[33]; + + if (!js) + WSContentSend_P(PSTR("
")); + + if (sc->sc_roomtemp.type != 0) + { + const struct miel_hvac_data_roomtemp *rt = + &sc->sc_roomtemp.data.roomtemp; + float t = (rt->temp05 != 0) + ? miel_hvac_roomtemp2deg(sc->sc_temp_type, rt->temp05) + : miel_hvac_roomtemp2deg(sc->sc_temp_type, rt->temp); + + dtostrfd(ConvertTemp(t), + Settings->flag2.temperature_resolution, buf); + miel_hvac_dsep(buf); + snprintf_P(val, sizeof(val), PSTR("%s " D_UNIT_DEGREE "%c"), buf, TempUnit()); + miel_hvac_web_ro(js, "hvro_room", "Room Temp", val); + } + + dtostrfd(ConvertTemp(miel_hvac_temp2deg(sc->sc_temp_type, traw)), + Settings->flag2.temperature_resolution, buf); + miel_hvac_dsep(buf); + snprintf_P(val, sizeof(val), PSTR("%s " D_UNIT_DEGREE "%c"), buf, TempUnit()); + miel_hvac_web_ro(js, "hvro_set", "Set Temp", val); + + name = set->power + ? miel_hvac_map_byval(set->mode & MIEL_HVAC_SETTINGS_MODE_MASK, + miel_hvac_mode_map, nitems(miel_hvac_mode_map)) + : "off"; + miel_hvac_web_ro(js, "hvro_mode", "Mode", name != NULL ? name : "-"); + + name = miel_hvac_map_byval(set->fan, + miel_hvac_fan_map, nitems(miel_hvac_fan_map)); + miel_hvac_web_ro(js, "hvro_fan", "Fan", name != NULL ? name : "-"); + + name = miel_hvac_map_byval(set->vane, + miel_hvac_vane_map, nitems(miel_hvac_vane_map)); + if (name == NULL) + name = "auto"; + + bool wv_isee = (set->widevane == 0x80 || set->widevane == 0x28 + || set->widevane == 0xaa); + + if (wv_isee) + vh = "isee"; + else + { + vh = miel_hvac_map_byval(set->widevane & MIEL_HVAC_SETTINGS_WIDEVANE_MASK, + miel_hvac_widevane_map, nitems(miel_hvac_widevane_map)); + if (vh == NULL) + { + snprintf_P(vhbuf, sizeof(vhbuf), PSTR("0x%02x"), set->widevane); + vh = vhbuf; + } + } + snprintf_P(val, sizeof(val), PSTR("%s / %s"), name, vh); + miel_hvac_web_ro(js, "hvro_vane", "Vane V / H", val); + + /* Air Direction — separate i-See function; "off" unless wide vane is + * in i-See mode. Shown only when the unit supports it. */ + if (!sc->sc_caps.sc_caps_valid + || (sc->sc_caps.cap_vane_v && sc->sc_has_isee)) + { + const char *ad = wv_isee + ? miel_hvac_map_byval(set->airdirection, + miel_hvac_airdirection_map, nitems(miel_hvac_airdirection_map)) + : "off"; + miel_hvac_web_ro(js, "hvro_dir", "Air Direction", + ad != NULL ? ad : "off"); + } + + if (sc->sc_stage.type != 0) + { + name = miel_hvac_map_byval(sc->sc_stage.data.stage.operation, + miel_hvac_stage_operation_map, + nitems(miel_hvac_stage_operation_map)); + miel_hvac_web_ro(js, "hvro_oper", "Operation", + name != NULL ? name : "-"); + } + + if (sc->sc_status.type != 0) + { + const struct miel_hvac_data_status *st = &sc->sc_status.data.status; + + snprintf_P(val, sizeof(val), PSTR("%u Hz"), st->compressorfrequency); + miel_hvac_web_ro(js, "hvro_comp", "Compressor", val); + + if (sc->sc_has_energy) + { + uint16_t p = ((uint16_t)st->operationpower << 8) | + (uint16_t)st->operationpower1; + dtostrfd((float)p, 0, buf); + miel_hvac_dsep(buf); + snprintf_P(val, sizeof(val), PSTR("%s " D_UNIT_WATT), buf); + miel_hvac_web_ro(js, "hvro_pow", "Power Usage", val); + + uint16_t e = ((uint16_t)st->operationenergy << 8) | + (uint16_t)st->operationenergy1; + dtostrfd((float)e / 10.0f, 1, buf); + miel_hvac_dsep(buf); + snprintf_P(val, sizeof(val), PSTR("%s " D_UNIT_KILOWATTHOUR), buf); + miel_hvac_web_ro(js, "hvro_egy", "Energy Total", val); + } + } + + if (js) + { + /* + * Keep the interactive controls in sync with the unit — but not + * while a change is still on its way to the unit (the settings we + * would sync from are stale then), and never yank a control the + * user is currently interacting with. + */ + if (!miel_hvac_update_settings_pending(sc) + && !miel_hvac_update_runstate_pending(sc)) + { + const char *fn = miel_hvac_map_byval(set->fan, + miel_hvac_fan_map, nitems(miel_hvac_fan_map)); + const char *vn = miel_hvac_map_byval(set->vane, + miel_hvac_vane_map, nitems(miel_hvac_vane_map)); + const char *wn = wv_isee ? NULL + : miel_hvac_map_byval(set->widevane & MIEL_HVAC_SETTINGS_WIDEVANE_MASK, + miel_hvac_widevane_map, nitems(miel_hvac_widevane_map)); + const char *ad = wv_isee + ? miel_hvac_map_byval(set->airdirection, + miel_hvac_airdirection_map, nitems(miel_hvac_airdirection_map)) + : "off"; + + WSContentSend_P(PSTR( + "function S(i,v){var s=eb(i);" + "if(s&&v&&s!==document.activeElement)s.value=v;}" + "var b=document.querySelectorAll('#hvacp .hp-seg .hp-b'),i;" + "for(i=0;isc_temp_type, traw), 1, tstr); + WSContentSend_P(PSTR("if(eb('hvtr')&&eb('hvtr')!==document.activeElement)" + "hvts(%s);"), tstr); + } + } + else + WSContentSend_P(PSTR("
")); +} + +/* + * FUNC_WEB_SENSOR — refresh the panel's read-only rows in place. Uses the + * same exec trick as other Tasmota drivers so the values + * update on every root-page poll without redrawing the interactive panel. */ static void miel_hvac_web_sensor(struct miel_hvac_softc *sc) { - if (sc->sc_status.type == 0 || !sc->sc_has_energy) + if (sc->sc_settings.type == 0) return; - const struct miel_hvac_data_status *status = - &sc->sc_status.data.status; - char buf[33]; + WSContentSend_P(PSTR("")); + WSContentSend_P(HTTP_MSG_EXEC_JAVASCRIPT); + WSContentSend_P(PSTR("var e;")); + miel_hvac_web_readout(sc, true); + WSContentSend_P(PSTR("\">{t}")); + WSContentSeparator(3); +} - uint16_t combined_power = - ((uint16_t)status->operationpower << 8) | - (uint16_t)status->operationpower1; - dtostrfd((float)combined_power, 0, buf); - WSContentSend_PD(PSTR("{s}" D_POWERUSAGE "{m}%s " D_UNIT_WATT "{e}"), buf); +/* + * Interactive HVAC control panel on the Tasmota main page. + * + * Rendered once per full page load from FUNC_WEB_ADD_MAIN_BUTTON (it is + * not part of the la() refresh region, so it keeps focus/selection while + * the sensor rows above update). Each control optimistically updates its + * own appearance and calls la('&='); the browser issues + * GET /?m=1&= and miel_hvac_web_getarg() (FUNC_WEB_GET_ARG) + * turns that into the matching HVACSet* console command. + * + * Which controls/options render is gated by the 0x7B 0xC9 Base + * Capabilities response (sc_caps); when capabilities are not yet known + * everything is shown and the unit rejects anything it cannot do. + */ +#define MIEL_HVAC_WEBARG_MODE "hvm" +#define MIEL_HVAC_WEBARG_TEMP "hvt" +#define MIEL_HVAC_WEBARG_FAN "hvf" +#define MIEL_HVAC_WEBARG_VANEV "hvv" +#define MIEL_HVAC_WEBARG_VANEH "hvh" +#define MIEL_HVAC_WEBARG_AIRDIR "hvd" +#define MIEL_HVAC_WEBARG_PURIFY "hvp" +#define MIEL_HVAC_WEBARG_NIGHT "hvn" +#define MIEL_HVAC_WEBARG_ECONO "hve" - uint16_t combined_energy = - ((uint16_t)status->operationenergy << 8) | - (uint16_t)status->operationenergy1; - dtostrfd((float)combined_energy / 10.0f, 1, buf); - WSContentSend_PD(PSTR("{s}" D_ENERGY_TOTAL "{m}%s " D_UNIT_KILOWATTHOUR "{e}"), buf); +/* + * Scoped stylesheet reproducing the design proposal exactly (dark palette, + * type scale, spacing). Everything is namespaced under #hvacp so it never + * touches the rest of the Tasmota page. The webfonts load when the device + * has internet; the fallback stack keeps the same metrics otherwise. + */ +static const char miel_hvac_web_style[] PROGMEM = + "" + ""; + +static const char miel_hvac_web_script[] PROGMEM = + ""; + +static void +miel_hvac_web_label(const char *label) +{ + WSContentSend_P(PSTR("
%s
"), label); +} + +static void +miel_hvac_web_modebtn(const char *v, const char *icon, const char *label, + const char *curm) +{ + WSContentSend_P(PSTR(""), + (strcmp(v, curm) == 0) ? "" : " off", v, v, icon, label); +} + +static void +miel_hvac_web_optbtn(const char *key, const char *label, bool on) +{ + WSContentSend_P(PSTR(""), + on ? "" : " off", on ? 1 : 0, key, label); +} + +/* + * Friendly display text for a protocol keyword. The