Add HVAC control panel to the MiEL HVAC web UI (xdrv_44) (#24984)

Replace the on/off-only main-page presence with a full climate panel and
a live read-only state table, styled to a fixed dark palette scoped
under #hvacp so it is independent of the Tasmota theme.

- FUNC_WEB_ADD_MAIN_BUTTON renders the panel once: mode segment
  (Auto/Heat/Cool/Dry/Fan/Off), target-temperature stepper + slider, and
  Fan speed / Vane vertical / Vane horizontal / Air direction selects;
  which controls appear is gated by the 0x7B 0xC9 Base Capabilities
  (cap_mode_*, cap_vane_v, cap_vane_swing, cap_fan_auto, cap_run_state)
  and miel_hvac_get_fan_count()
- controls call la('&<key>=<value>'); FUNC_WEB_GET_ARG turns each key
  into the matching HVACSet* command (mode via HVACSetHAMode so a mode
  button also powers the unit), reusing the existing validated setters
- FUNC_WEB_SENSOR refreshes the panel in place every web_refresh via the
  <img onerror> exec trick: state-table cells, mode-button highlight and
  every select value, skipped while a change is still in flight
  (update_settings/runstate_pending) or the control is focused
- state table: Room/Set temp, Mode, Fan, Vane V/H, Air Direction,
  Operation, Compressor, Power/Energy; locale decimal separator applied

(cherry picked from commit 6e013484f168f3748f6d388f18ef69f8313fab97)

Co-authored-by: Theo Arends <11044339+arendst@users.noreply.github.com>
This commit is contained in:
Grzegorz
2026-08-30 22:17:06 +02:00
committed by GitHub
co-authored by Theo Arends
parent 41964bb72d
commit 2faea5078a
2 changed files with 633 additions and 17 deletions
+1
View File
@@ -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
+632 -17
View File
@@ -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 <tr> into the panel
* card (miel_hvac_web_panel); js=true emits a JS statement that refreshes
* that same row's <td> 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("<tr><th>%s</th><td id='%s'>%s</td></tr>"),
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("<div class='hp-ro-w'><table class='hp-ro'>"));
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;i<b.length;i++){"
"b[i].className=(b[i].dataset.m=='%s')?'hp-b':'hp-b off';}"),
miel_hvac_web_curmode(set));
if (fn != NULL)
WSContentSend_P(PSTR("S('hvf','%s');"), fn);
if (vn != NULL)
WSContentSend_P(PSTR("S('hvv','%s');"), vn);
if (wn != NULL)
WSContentSend_P(PSTR("S('hvh','%s');"), wn);
if (ad != NULL)
WSContentSend_P(PSTR("S('hvd','%s');"), ad);
/* target temperature: reuse the panel's own hvts() setter */
char tstr[12];
dtostrfd(miel_hvac_temp2deg(sc->sc_temp_type, traw), 1, tstr);
WSContentSend_P(PSTR("if(eb('hvtr')&&eb('hvtr')!==document.activeElement)"
"hvts(%s);"), tstr);
}
}
else
WSContentSend_P(PSTR("</table></div>"));
}
/*
* FUNC_WEB_SENSOR — refresh the panel's read-only rows in place. Uses the
* same <img onerror> 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("</table>"));
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('&<key>=<value>'); the browser issues
* GET /?m=1&<key>=<value> 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 =
"<link rel='stylesheet' href='https://fonts.googleapis.com/css2?"
"family=Barlow:wght@400;500;600&amp;"
"family=Barlow+Semi+Condensed:wght@600;700&amp;display=swap'>"
"<style>"
"#hvacp{margin-top:10px;text-align:left;color:#e7eaec;font-size:14px;line-height:1.4;"
"font-family:'Barlow','Segoe UI',system-ui,-apple-system,sans-serif}"
"#hvacp *{box-sizing:border-box}"
"#hvacp,#hvacp div,#hvacp input,#hvacp table,#hvacp td,#hvacp th{padding:0;margin:0}"
"#hvacp .hp-panel{background:#262a2d;border:1px solid #4a5054;border-radius:8px;padding:12px;"
"box-shadow:0 1px 2px rgba(0,0,0,.4),0 10px 34px rgba(0,0,0,.45)}"
"#hvacp .hp-field{margin-bottom:12px}"
"#hvacp .hp-field:last-child{margin-bottom:0}"
"#hvacp .hp-label{font-family:'Barlow Semi Condensed','Barlow',system-ui,sans-serif;"
"text-transform:uppercase;letter-spacing:.06em;font-size:11px;font-weight:600;"
"color:#9aa1a7;margin-bottom:5px}"
"#hvacp .hp-seg{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px}"
"#hvacp .hp-b{font-family:'Barlow Semi Condensed','Barlow',system-ui,sans-serif;font-weight:600;"
"font-size:13px;line-height:1.3;color:#08151d;background:#1fa3ec;border:0;border-radius:6px;"
"padding:8px 4px;width:100%%;cursor:pointer;transition:filter .12s ease}"
"#hvacp .hp-b:hover{filter:brightness(1.06)}"
"#hvacp .hp-b.off{background:#565c61;color:#e7eaec}"
"#hvacp .hp-b:focus-visible,#hvacp .hp-sel:focus-visible{outline:2px solid #1fa3ec;outline-offset:1px}"
"#hvacp .hp-seg .hp-b{display:flex;flex-direction:column;align-items:center;gap:2px;line-height:1.15}"
"#hvacp .hp-ico{font-size:15px;line-height:1}"
"#hvacp .hp-temp{display:flex;align-items:center;gap:10px}"
"#hvacp .hp-temp .hp-b{width:40px;height:40px;font-size:20px;flex:none;padding:0}"
"#hvacp .hp-val{font-family:'Barlow Semi Condensed','Barlow',system-ui,sans-serif;font-weight:700;"
"font-size:26px;flex:1;text-align:center;font-variant-numeric:tabular-nums}"
"#hvacp .hp-val small{font-size:14px;font-weight:600;color:#9aa1a7}"
"#hvacp .hp-sel{width:100%%;font-family:'Barlow','Segoe UI',system-ui,sans-serif;font-size:14px;"
"color:#e7eaec;background:#1f2325;border:1px solid #4a5054;border-radius:6px;padding:8px 9px;"
"cursor:pointer}"
"#hvacp input[type=range]{width:100%%;accent-color:#1fa3ec;margin-top:8px;display:block}"
"#hvacp .hp-toggles{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}"
"#hvacp .hp-toggles .hp-b{font-size:12px}"
"#hvacp .hp-ro-w{padding-top:10px;border-top:1px solid #4a5054}"
"#hvacp .hp-ro{width:100%%;border-collapse:collapse;font-size:13px}"
"#hvacp .hp-ro th{text-align:left;font-weight:500;color:#9aa1a7;padding:3px 0}"
"#hvacp .hp-ro td{text-align:right;font-variant-numeric:tabular-nums;padding:3px 0;white-space:nowrap}"
"</style>";
static const char miel_hvac_web_script[] PROGMEM =
"<script>"
"function hvm(b,v){"
"var q=b.parentNode.getElementsByTagName('button');"
"for(var i=0;i<q.length;i++){q[i].className='hp-b off';}"
"b.className='hp-b';"
"la('&" MIEL_HVAC_WEBARG_MODE "='+v);"
"}"
"function hvo(b,k){"
"var o=(b.dataset.on=='1')?0:1;"
"b.dataset.on=o;"
"b.className=o?'hp-b':'hp-b off';"
"la('&'+k+'='+(o?'on':'off'));"
"}"
"function hvts(t){"
"t=Math.round(t/hvcs)*hvcs;"
"if(t<hvcl){t=hvcl;}if(t>hvch){t=hvch;}"
"hvct=t;"
"eb('hvtv').innerHTML=t.toFixed(1)+'<small>&deg;C</small>';"
"eb('hvtr').value=t;"
"}"
"function hvtb(d){hvts(hvct+d);la('&" MIEL_HVAC_WEBARG_TEMP "='+hvct.toFixed(1));}"
"function hvsl(v){hvts(v*1);la('&" MIEL_HVAC_WEBARG_TEMP "='+hvct.toFixed(1));}"
"</script>";
static void
miel_hvac_web_label(const char *label)
{
WSContentSend_P(PSTR("<div class='hp-label'>%s</div>"), label);
}
static void
miel_hvac_web_modebtn(const char *v, const char *icon, const char *label,
const char *curm)
{
WSContentSend_P(PSTR("<button class='hp-b%s' data-m='%s' onclick=\"hvm(this,'%s')\">"
"<span class='hp-ico'>%s</span>%s</button>"),
(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("<button class='hp-b%s' data-on='%d' "
"onclick=\"hvo(this,'%s')\">%s</button>"),
on ? "" : " off", on ? 1 : 0, key, label);
}
/*
* Friendly display text for a protocol keyword. The <option> value stays
* the machine name so the emitted HVACSet* command is unchanged.
*/
static const char *
miel_hvac_web_optlabel(const char *name)
{
static const struct {
const char *k;
const char *v;
} lbl[] = {
{ "auto", "Auto" },
{ "quiet", "Quiet" },
{ "up", "Up" },
{ "up_middle", "Up-Middle" },
{ "center", "Center" },
{ "down_middle", "Down-Middle" },
{ "down", "Down" },
{ "swing", "Swing" },
{ "left", "Left" },
{ "left_middle", "Left-Middle" },
{ "right", "Right" },
{ "right_middle", "Right-Middle" },
{ "left_center", "Left-Center" },
{ "right_center", "Right-Center" },
{ "split", "Split" },
{ "even", "Even" },
{ "indirect", "Indirect" },
{ "direct", "Direct" },
{ "off", "Off" },
};
size_t i;
for (i = 0; i < nitems(lbl); i++)
{
if (strcmp(name, lbl[i].k) == 0)
return (lbl[i].v);
}
return (name);
}
static void
miel_hvac_web_select(const char *label, const char *id, const char *key,
const struct miel_hvac_map *m, size_t n, uint8_t cur,
const uint8_t *skip, size_t nskip)
{
size_t i, j;
WSContentSend_P(PSTR("<div class='hp-field'>"));
miel_hvac_web_label(label);
WSContentSend_P(PSTR("<select class='hp-sel' id='%s' onchange=\"la('&%s='+this.value)\">"),
id, key);
for (i = 0; i < n; i++)
{
bool skipit = false;
for (j = 0; j < nskip; j++)
{
if (skip[j] == m[i].byte)
{
skipit = true;
break;
}
}
if (skipit)
continue;
WSContentSend_P(PSTR("<option value='%s'%s>%s</option>"),
m[i].name, (m[i].byte == cur) ? " selected" : "",
miel_hvac_web_optlabel(m[i].name));
}
WSContentSend_P(PSTR("</select></div>"));
}
static void
miel_hvac_web_panel(struct miel_hvac_softc *sc)
{
if (sc->sc_settings.type == 0)
return;
const struct miel_hvac_data_settings *set = &sc->sc_settings.data.settings;
const struct miel_hvac_capabilities *caps = &sc->sc_caps;
bool cv = caps->sc_caps_valid;
bool m_heat = !cv || caps->cap_mode_heat;
bool m_dry = !cv || caps->cap_mode_dry;
bool m_fan = !cv || caps->cap_mode_fan;
const char *curm = miel_hvac_web_curmode(set);
uint8_t traw = (set->temp05 != 0) ? set->temp05 : set->temp;
float curtemp = miel_hvac_temp2deg(sc->sc_temp_type, traw);
int tlo = 16, thi = 31;
char tbuf[16];
if (cv && caps->cap_temp_ranges)
{
uint8_t mm = set->mode & MIEL_HVAC_SETTINGS_MODE_MASK;
if (mm == MIEL_HVAC_SETTINGS_MODE_HEAT)
{
tlo = (caps->temp_heat_min - 128) / 2;
thi = (caps->temp_heat_max - 128) / 2;
}
else if (mm == MIEL_HVAC_SETTINGS_MODE_AUTO)
{
tlo = (caps->temp_auto_min - 128) / 2;
thi = (caps->temp_auto_max - 128) / 2;
}
else
{
tlo = (caps->temp_cool_min - 128) / 2;
thi = (caps->temp_cool_max - 128) / 2;
}
}
dtostrfd(curtemp, 1, tbuf);
WSContentSend_P(miel_hvac_web_style);
WSContentSend_P(PSTR("<div id='hvacp'><div class='hp-panel'>"));
/* per-page JS state, then the shared helpers */
WSContentSend_P(PSTR("<script>var hvct=%s,hvcs=%s,hvcl=%d,hvch=%d;</script>"),
tbuf, sc->sc_temp_type ? "0.5" : "1", tlo, thi);
WSContentSend_P(miel_hvac_web_script);
/* Mode */
miel_hvac_web_label("Mode");
WSContentSend_P(PSTR("<div class='hp-seg'>"));
miel_hvac_web_modebtn("auto", "A", "Auto", curm);
if (m_heat)
miel_hvac_web_modebtn("heat", "\xe2\x98\x80", "Heat", curm);
miel_hvac_web_modebtn("cool", "\xe2\x9d\x84", "Cool", curm);
if (m_dry)
miel_hvac_web_modebtn("dry", "\xf0\x9f\x92\xa7", "Dry", curm);
if (m_fan)
miel_hvac_web_modebtn("fan", "\xe2\x9c\xb1", "Fan", curm);
miel_hvac_web_modebtn("off", "\xe2\x8f\xbb", "Off", curm);
WSContentSend_P(PSTR("</div>"));
/* Target temperature */
WSContentSend_P(PSTR("<div class='hp-field'>"));
miel_hvac_web_label("Target temperature");
WSContentSend_P(PSTR("<div class='hp-temp'>"
"<button class='hp-b' onclick='hvtb(-hvcs)'>&minus;</button>"
"<div class='hp-val' id='hvtv'>%s<small>&deg;C</small></div>"
"<button class='hp-b' onclick='hvtb(hvcs)'>+</button></div>"
"<input type='range' id='hvtr' min='%d' max='%d' step='%s' value='%s' "
"onchange='hvsl(this.value)'></div>"),
tbuf, tlo, thi, sc->sc_temp_type ? "0.5" : "1", tbuf);
/* Fan speed */
{
uint8_t fskip[3];
size_t nf = 0;
uint8_t fc = miel_hvac_get_fan_count(sc);
if (cv && !caps->cap_fan_auto)
fskip[nf++] = MIEL_HVAC_SETTINGS_FAN_AUTO;
if (fc != 0 && fc < 5)
fskip[nf++] = MIEL_HVAC_SETTINGS_FAN_QUIET;
if (fc != 0 && fc < 4)
fskip[nf++] = MIEL_HVAC_SETTINGS_FAN_4;
miel_hvac_web_select("Fan speed", "hvf", MIEL_HVAC_WEBARG_FAN,
miel_hvac_fan_map, nitems(miel_hvac_fan_map),
set->fan, fskip, nf);
}
/* Vane vertical — only when the unit has a controllable vertical vane */
if (!cv || caps->cap_vane_v)
{
uint8_t vskip[1];
size_t nv = 0;
if (cv && !caps->cap_vane_swing)
vskip[nv++] = MIEL_HVAC_SETTINGS_VANE_SWING;
miel_hvac_web_select("Vane vertical", "hvv", MIEL_HVAC_WEBARG_VANEV,
miel_hvac_vane_map, nitems(miel_hvac_vane_map),
set->vane, vskip, nv);
}
/* Vane horizontal / wide vane */
{
static const uint8_t hskip[] = { MIEL_HVAC_SETTINGS_WIDEVANE_ISEE };
miel_hvac_web_select("Vane horizontal", "hvh", MIEL_HVAC_WEBARG_VANEH,
miel_hvac_widevane_map, nitems(miel_hvac_widevane_map),
set->widevane & MIEL_HVAC_SETTINGS_WIDEVANE_MASK,
hskip, nitems(hskip));
}
/* Air direction (i-See) — separate function; needs a vertical vane and
* an observed i-See sensor. Direction is only meaningful while the wide
* vane is in i-See mode; otherwise the control reads "off". */
if (!cv || (caps->cap_vane_v && sc->sc_has_isee))
{
bool wv_isee = (set->widevane == 0x80 || set->widevane == 0x28
|| set->widevane == 0xaa);
uint8_t adcur = wv_isee
? set->airdirection
: MIEL_HVAC_SETTINGS_AIRDIRECTION_OFF;
miel_hvac_web_select("Air direction", "hvd", MIEL_HVAC_WEBARG_AIRDIR,
miel_hvac_airdirection_map, nitems(miel_hvac_airdirection_map),
adcur, NULL, 0);
}
/* Purifier / Night mode / EconoCool (0x08 Set Run State) */
if (!cv || caps->cap_run_state)
{
const struct miel_hvac_data_options *opt = &sc->sc_options.data.options;
bool have = (sc->sc_options.type != 0);
WSContentSend_P(PSTR("<div class='hp-field'>"));
miel_hvac_web_label("Options");
WSContentSend_P(PSTR("<div class='hp-toggles'>"));
miel_hvac_web_optbtn(MIEL_HVAC_WEBARG_PURIFY, "Purifier",
have && opt->purifier == MIEL_HVAC_OPTIONS_PURIFIER_ON);
miel_hvac_web_optbtn(MIEL_HVAC_WEBARG_NIGHT, "Night",
have && opt->nightmode == MIEL_HVAC_OPTIONS_NIGHTMODE_ON);
miel_hvac_web_optbtn(MIEL_HVAC_WEBARG_ECONO, "EconoCool",
have && opt->econocool == MIEL_HVAC_OPTIONS_ECONOCOOL_ON);
WSContentSend_P(PSTR("</div></div>"));
}
miel_hvac_web_readout(sc, false);
WSContentSend_P(PSTR("</div></div>")); /* close .hp-panel, #hvacp */
}
/*
* FUNC_WEB_GET_ARG — turn a control-panel GET argument into the matching
* HVACSet* console command. Mode uses HVACSetHAMode so a mode button also
* powers the unit on and "Off" powers it down.
*/
static void
miel_hvac_web_getarg(void)
{
char tmp[24];
char cmnd[48];
#define MIEL_HVAC_WEB_GETARG(_k, _c) do { \
WebGetArg(PSTR(_k), tmp, sizeof(tmp)); \
if (strlen(tmp)) \
{ \
snprintf_P(cmnd, sizeof(cmnd), PSTR(_c " %s"), tmp); \
ExecuteWebCommand(cmnd); \
} \
} while (0)
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_MODE, D_CMND_MIEL_HVAC_SETHAMODE);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_TEMP, D_CMND_MIEL_HVAC_SETTEMP);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_FAN, D_CMND_MIEL_HVAC_SETFANSPEED);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_VANEV, D_CMND_MIEL_HVAC_SETSWINGV);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_VANEH, D_CMND_MIEL_HVAC_SETSWINGH);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_AIRDIR, D_CMND_MIEL_HVAC_SETAIRDIRECTION);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_PURIFY, D_CMND_MIEL_HVAC_SETPURIFY);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_NIGHT, D_CMND_MIEL_HVAC_SETNIGHTMODE);
MIEL_HVAC_WEB_GETARG(MIEL_HVAC_WEBARG_ECONO, D_CMND_MIEL_HVAC_SETECONOCOOL);
#undef MIEL_HVAC_WEB_GETARG
}
#endif /* USE_WEBSERVER */
@@ -3860,6 +4469,12 @@ bool Xdrv44(uint32_t function)
case FUNC_WEB_SENSOR:
miel_hvac_web_sensor(sc);
break;
case FUNC_WEB_ADD_MAIN_BUTTON:
miel_hvac_web_panel(sc);
break;
case FUNC_WEB_GET_ARG:
miel_hvac_web_getarg();
break;
#endif
case FUNC_AFTER_TELEPERIOD:
if (sc->sc_settings_set)