/* xdrv_44_miel_hvac.ino - Mitsubishi Electric HVAC support for Tasmota Copyright (C) 2020 David Gwynne This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_MIEL_HVAC /*********************************************************************************************\ * Mitsubishi Electric HVAC serial interface \*********************************************************************************************/ #define XDRV_44 44 #define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) #define CTASSERT(x) extern char _ctassert[(x) ? 1 : -1 ] \ __attribute__((__unused__)) #define MIEL_HVAC_LOGNAME "MiElHVAC" #define D_CMND_MIEL_HVAC_SETFANSPEED "HVACSetFanSpeed" #define D_CMND_MIEL_HVAC_SETMODE "HVACSetMode" #define D_CMND_MIEL_HVAC_SETHAMODE "HVACSetHAMode" #define D_CMND_MIEL_HVAC_SETTEMP "HVACSetTemp" #define D_CMND_MIEL_HVAC_SETSWINGV "HVACSetSwingV" #define D_CMND_MIEL_HVAC_SETSWINGH "HVACSetSwingH" #define D_CMND_MIEL_HVAC_REMOTETEMP "HVACRemoteTemp" #include /* from hvac */ struct miel_hvac_header { uint8_t start; #define MIEL_HVAC_H_START 0xfc uint8_t type; #define MIEL_HVAC_H_TYPE_UPDATED 0x61 #define MIEL_HVAC_H_TYPE_DATA 0x62 #define MIEL_HVAC_H_TYPE_CONNECTED 0x7a uint8_t middle1; #define MIEL_HVAC_H_MIDDLE1 0x01 uint8_t middle2; #define MIEL_HVAC_H_MIDDLE2 0x30 uint8_t len; }; struct miel_hvac_data_settings { uint8_t _pad1[2]; uint8_t power; uint8_t mode; #define MIEL_HVAC_SETTINGS_MODE_MASK 0x7f uint8_t temp; uint8_t fan; uint8_t vane; uint8_t _pad2[2]; uint8_t widevane; #define MIEL_HVAC_SETTTINGS_WIDEVANE_MASK \ 0x0f }; struct miel_hvac_data_roomtemp { uint8_t _pad1[2]; uint8_t temp; }; struct miel_hvac_data_status { uint8_t _pad1[2]; uint8_t compressor; uint8_t operation; }; struct miel_hvac_data { uint8_t type; #define MIEL_HVAC_DATA_T_SETTINGS 0x02 #define MIEL_HVAC_DATA_T_ROOMTEMP 0x03 #define MIEL_HVAC_DATA_T_TIMER 0x05 #define MIEL_HVAC_DATA_T_STATUS 0x06 #define MIEL_HVAC_DATA_T_STAGE 0x09 union { struct miel_hvac_data_settings settings; struct miel_hvac_data_roomtemp roomtemp; struct miel_hvac_data_status status; uint8_t bytes[15]; } data; }; CTASSERT(sizeof(struct miel_hvac_data) == 16); CTASSERT(offsetof(struct miel_hvac_data, data.settings.power) == 3); CTASSERT(offsetof(struct miel_hvac_data, data.settings.mode) == 4); CTASSERT(offsetof(struct miel_hvac_data, data.settings.temp) == 5); CTASSERT(offsetof(struct miel_hvac_data, data.settings.fan) == 6); CTASSERT(offsetof(struct miel_hvac_data, data.settings.vane) == 7); CTASSERT(offsetof(struct miel_hvac_data, data.settings.widevane) == 10); CTASSERT(offsetof(struct miel_hvac_data, data.roomtemp.temp) == 3); /* to hvac */ #define MIEL_HVAC_H_TYPE_CONNECT 0x5a static const uint8_t miel_hvac_msg_connect[] = { 0xca, 0x01 }; #define MIEL_HVAC_H_TYPE_REQUEST 0x42 struct miel_hvac_msg_request { uint8_t type; #define MIEL_HVAC_REQUEST_SETTINGS 0x02 #define MIEL_HVAC_REQUEST_ROOMTEMP 0x03 #define MIEL_HVAC_REQUEST_TIMERS 0x05 #define MIEL_HVAC_REQUEST_STATUS 0x06 #define MIEL_HVAC_REQUEST_STAGE 0x09 uint8_t zero[15]; }; #define MIEL_HVAC_H_TYPE_UPDATE 0x41 struct miel_hvac_msg_update { uint8_t one; uint16_t flags; #define MIEL_HVAC_UPDATE_F_WIDEVANE (1 << 0) #define MIEL_HVAC_UPDATE_F_POWER (1 << 8) #define MIEL_HVAC_UPDATE_F_MODE (1 << 9) #define MIEL_HVAC_UPDATE_F_TEMP (1 << 10) #define MIEL_HVAC_UPDATE_F_FAN (1 << 11) #define MIEL_HVAC_UPDATE_F_VANE (1 << 12) uint8_t power; #define MIEL_HVAC_UPDATE_POWER_OFF 0x00 #define MIEL_HVAC_UPDATE_POWER_ON 0x01 uint8_t mode; #define MIEL_HVAC_UPDATE_MODE_HEAT 0x01 #define MIEL_HVAC_UPDATE_MODE_DRY 0x02 #define MIEL_HVAC_UPDATE_MODE_COOL 0x03 #define MIEL_HVAC_UPDATE_MODE_FAN 0x07 #define MIEL_HVAC_UPDATE_MODE_AUTO 0x08 uint8_t temp; #define MIEL_HVAC_UPDATE_TEMP_MIN 16 #define MIEL_HVAC_UPDATE_TEMP_MAX 31 uint8_t fan; #define MIEL_HVAC_UPDATE_FAN_AUTO 0x00 #define MIEL_HVAC_UPDATE_FAN_QUIET 0x01 #define MIEL_HVAC_UPDATE_FAN_1 0x02 #define MIEL_HVAC_UPDATE_FAN_2 0x03 #define MIEL_HVAC_UPDATE_FAN_3 0x05 #define MIEL_HVAC_UPDATE_FAN_4 0x06 uint8_t vane; #define MIEL_HVAC_UPDATE_VANE_AUTO 0x00 #define MIEL_HVAC_UPDATE_VANE_1 0x01 #define MIEL_HVAC_UPDATE_VANE_2 0x02 #define MIEL_HVAC_UPDATE_VANE_3 0x03 #define MIEL_HVAC_UPDATE_VANE_4 0x04 #define MIEL_HVAC_UPDATE_VANE_5 0x05 #define MIEL_HVAC_UPDATE_VANE_SWING 0x07 uint8_t _pad1[5]; uint8_t widevane; #define MIEL_HVAC_UPDATE_WIDEVANE_MASK 0x0f #define MIEL_HVAC_UPDATE_WIDEVANE_LL 0x01 #define MIEL_HVAC_UPDATE_WIDEVANE_L 0x02 #define MIEL_HVAC_UPDATE_WIDEVANE_LL 0x01 #define MIEL_HVAC_UPDATE_WIDEVANE_L 0x02 #define MIEL_HVAC_UPDATE_WIDEVANE_C 0x03 #define MIEL_HVAC_UPDATE_WIDEVANE_R 0x04 #define MIEL_HVAC_UPDATE_WIDEVANE_RR 0x05 #define MIEL_HVAC_UPDATE_WIDEVANE_LR 0x08 #define MIEL_HVAC_UPDATE_WIDEVANE_SWING 0x0c #define MIEL_HVAC_UPDATE_WIDEVANE_ADJ 0x80 uint8_t _pad2[2]; } __packed; CTASSERT(sizeof(struct miel_hvac_msg_update) == 16); #define MIEL_HVAC_OFFS(_v) ((_v) - sizeof(struct miel_hvac_header)) CTASSERT(offsetof(struct miel_hvac_msg_update, flags) == MIEL_HVAC_OFFS(6)); CTASSERT(offsetof(struct miel_hvac_msg_update, power) == MIEL_HVAC_OFFS(8)); CTASSERT(offsetof(struct miel_hvac_msg_update, mode) == MIEL_HVAC_OFFS(9)); CTASSERT(offsetof(struct miel_hvac_msg_update, temp) == MIEL_HVAC_OFFS(10)); CTASSERT(offsetof(struct miel_hvac_msg_update, fan) == MIEL_HVAC_OFFS(11)); CTASSERT(offsetof(struct miel_hvac_msg_update, vane) == MIEL_HVAC_OFFS(12)); CTASSERT(offsetof(struct miel_hvac_msg_update, widevane) == MIEL_HVAC_OFFS(18)); static inline uint8_t miel_hvac_deg2temp(uint8_t deg) { return (31 - deg); } static inline uint8_t miel_hvac_temp2deg(uint8_t temp) { return (31 - temp); } static inline unsigned int miel_hvac_roomtemp2deg(uint8_t roomtemp) { return ((unsigned int)roomtemp + 10); } struct miel_hvac_msg_remotetemp { uint8_t seven; uint8_t control; #define MIEL_HVAC_REMOTETEMP_CLR 0x00 #define MIEL_HVAC_REMOTETEMP_SET 0x01 /* setting for older units expressed as .5C units starting at 8C */ uint8_t temp_old; #define MIEL_HVAC_REMOTETEMP_OLD_MIN 8 #define MIEL_HVAC_REMOTETEMP_OLD_MAX 38 #define MIEL_HVAC_REMOTETEMP_OLD_FACTOR 2 /* setting for newer units expressed as .5C units starting at -63C */ uint8_t temp; #define MIEL_HVAC_REMOTETEMP_MIN -63 #define MIEL_HVAC_REMOTETEMP_MAX 63 #define MIEL_HVAC_REMOTETEMP_OFFSET 64 #define MIEL_HVAC_REMOTETEMP_FACTOR 2 uint8_t _pad2[12]; }; CTASSERT(sizeof(struct miel_hvac_msg_remotetemp) == 16); static inline uint8_t miel_hvac_cksum_fini(uint8_t sum) { return (0xfc - sum); } struct miel_hvac_map { uint8_t byte; const char *name; }; static const struct miel_hvac_map miel_hvac_mode_map[] = { { MIEL_HVAC_UPDATE_MODE_HEAT, "heat" }, { MIEL_HVAC_UPDATE_MODE_DRY, "dry" }, { MIEL_HVAC_UPDATE_MODE_COOL, "cool" }, { MIEL_HVAC_UPDATE_MODE_FAN, "fan_only" }, { MIEL_HVAC_UPDATE_MODE_AUTO, "auto" }, }; static const struct miel_hvac_map miel_hvac_fan_map[] = { { MIEL_HVAC_UPDATE_FAN_AUTO, "auto" }, { MIEL_HVAC_UPDATE_FAN_QUIET, "quiet" }, { MIEL_HVAC_UPDATE_FAN_1, "1" }, { MIEL_HVAC_UPDATE_FAN_2, "2" }, { MIEL_HVAC_UPDATE_FAN_3, "3" }, { MIEL_HVAC_UPDATE_FAN_4, "4" }, }; static const struct miel_hvac_map miel_hvac_vane_map[] = { { MIEL_HVAC_UPDATE_VANE_AUTO, "auto" }, { MIEL_HVAC_UPDATE_VANE_1, "1" }, { MIEL_HVAC_UPDATE_VANE_2, "2" }, { MIEL_HVAC_UPDATE_VANE_3, "3" }, { MIEL_HVAC_UPDATE_VANE_4, "4" }, { MIEL_HVAC_UPDATE_VANE_5, "5" }, { MIEL_HVAC_UPDATE_VANE_SWING, "swing" }, }; static const struct miel_hvac_map miel_hvac_widevane_map[] = { { MIEL_HVAC_UPDATE_WIDEVANE_LL, "LL" }, { MIEL_HVAC_UPDATE_WIDEVANE_L, "L" }, { MIEL_HVAC_UPDATE_WIDEVANE_C, "C" }, { MIEL_HVAC_UPDATE_WIDEVANE_R, "R" }, { MIEL_HVAC_UPDATE_WIDEVANE_RR, "RR" }, { MIEL_HVAC_UPDATE_WIDEVANE_LR, "split" }, { MIEL_HVAC_UPDATE_WIDEVANE_SWING, "swing" }, }; enum miel_hvac_parser_state { MIEL_HVAC_P_START, MIEL_HVAC_P_TYPE, MIEL_HVAC_P_MIDDLE1, MIEL_HVAC_P_MIDDLE2, MIEL_HVAC_P_LEN, MIEL_HVAC_P_DATA, MIEL_HVAC_P_CKSUM, MIEL_HVAC_P_SKIP, MIEL_HVAC_P_SKIP_CKSUM, }; #define MIEL_HVAC_DATABUFLEN 64 struct miel_hvac_parser { enum miel_hvac_parser_state p_state; uint8_t p_type; uint8_t p_sum; uint8_t p_len; uint8_t p_off; uint8_t p_data[MIEL_HVAC_DATABUFLEN]; }; struct miel_hvac_softc { TasmotaSerial *sc_serial; struct miel_hvac_parser sc_parser; unsigned int sc_device; unsigned int sc_tick; bool sc_settings_set; bool sc_connected; struct miel_hvac_data sc_settings; struct miel_hvac_data sc_temp; struct miel_hvac_data sc_status; struct miel_hvac_data sc_stage; struct miel_hvac_msg_update sc_update; struct miel_hvac_msg_remotetemp sc_remotetemp; }; static inline bool miel_hvac_update_pending(struct miel_hvac_softc *sc) { struct miel_hvac_msg_update *update = &sc->sc_update; return (update->flags != htons(0)); } static struct miel_hvac_softc *miel_hvac_sc = nullptr; static void miel_hvac_input_connected(struct miel_hvac_softc *, const void *, size_t); static void miel_hvac_input_data(struct miel_hvac_softc *, const void *, size_t); static void miel_hvac_input_updated(struct miel_hvac_softc *, const void *, size_t); static enum miel_hvac_parser_state miel_hvac_parse(struct miel_hvac_softc *sc, uint8_t byte) { struct miel_hvac_parser *p = &sc->sc_parser; enum miel_hvac_parser_state nstate = p->p_state; switch (p->p_state) { case MIEL_HVAC_P_START: if (byte != MIEL_HVAC_H_START) return (MIEL_HVAC_P_START); /* reset state */ p->p_sum = 0; nstate = MIEL_HVAC_P_TYPE; break; case MIEL_HVAC_P_TYPE: p->p_type = byte; nstate = MIEL_HVAC_P_MIDDLE1; break; case MIEL_HVAC_P_MIDDLE1: if (byte != MIEL_HVAC_H_MIDDLE1) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": parse state MIDDLE1 expected %02x got %02x" ", restarting"), MIEL_HVAC_H_MIDDLE1, byte); return (MIEL_HVAC_P_START); } nstate = MIEL_HVAC_P_MIDDLE2; break; case MIEL_HVAC_P_MIDDLE2: if (byte != MIEL_HVAC_H_MIDDLE2) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": parse state MIDDLE2 expected %02x got %02x" ", restarting"), MIEL_HVAC_H_MIDDLE2, byte); return (MIEL_HVAC_P_START); } nstate = MIEL_HVAC_P_LEN; break; case MIEL_HVAC_P_LEN: if (byte == 0) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": skipping 0 byte message type 0x%02x"), p->p_type); return (MIEL_HVAC_P_SKIP_CKSUM); } p->p_len = byte; p->p_off = 0; switch (p->p_type) { case MIEL_HVAC_H_TYPE_CONNECTED: case MIEL_HVAC_H_TYPE_DATA: case MIEL_HVAC_H_TYPE_UPDATED: break; default: AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": skipping unknown message type 0x%02x"), p->p_type); return (MIEL_HVAC_P_SKIP); } if (byte > sizeof(p->p_data)) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": skipping %u data bytes of message type 0x%02x"), p->p_len, p->p_type); return (MIEL_HVAC_P_SKIP); } nstate = MIEL_HVAC_P_DATA; break; case MIEL_HVAC_P_DATA: p->p_data[p->p_off++] = byte; if (p->p_off >= p->p_len) nstate = MIEL_HVAC_P_CKSUM; break; case MIEL_HVAC_P_CKSUM: if (miel_hvac_cksum_fini(p->p_sum) != byte) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": checksum failed, restarting")); return (MIEL_HVAC_P_START); } switch (p->p_type) { case MIEL_HVAC_H_TYPE_CONNECTED: miel_hvac_input_connected(sc, p->p_data, p->p_len); break; case MIEL_HVAC_H_TYPE_DATA: miel_hvac_input_data(sc, p->p_data, p->p_len); break; case MIEL_HVAC_H_TYPE_UPDATED: miel_hvac_input_updated(sc, p->p_data, p->p_len); break; } /* this message is done, wait for another */ return (MIEL_HVAC_P_START); case MIEL_HVAC_P_SKIP: if (++p->p_off >= p->p_len) return (MIEL_HVAC_P_SKIP_CKSUM); return (nstate); case MIEL_HVAC_P_SKIP_CKSUM: return (MIEL_HVAC_P_START); } p->p_sum += byte; return (nstate); } static uint8_t miel_hvac_write(struct miel_hvac_softc *sc, const uint8_t *bytes, size_t len) { TasmotaSerial *serial = sc->sc_serial; uint8_t cksum = 0; size_t i; for (i = 0; i < len; i++) { uint8_t b = bytes[i]; serial->write(b); cksum += b; } return (cksum); } static void miel_hvac_send(struct miel_hvac_softc *sc, uint8_t type, const void *data, size_t len) { TasmotaSerial *serial = sc->sc_serial; struct miel_hvac_header h = { MIEL_HVAC_H_START, type, MIEL_HVAC_H_MIDDLE1, MIEL_HVAC_H_MIDDLE2, (uint8_t)len, }; uint8_t cksum = 0; cksum += miel_hvac_write(sc, (const uint8_t *)&h, sizeof(h)); cksum += miel_hvac_write(sc, (const uint8_t *)data, len); char hex_h[(sizeof(h) + 1) * 2]; char hex_d[(len + 1) * 2]; AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": sending %s %s %02x"), ToHex_P((uint8_t *)&h, sizeof(h), hex_h, sizeof(hex_h)), ToHex_P((uint8_t *)data, len, hex_d, sizeof(hex_d)), miel_hvac_cksum_fini(cksum)); serial->write(miel_hvac_cksum_fini(cksum)); serial->flush(); } #define miel_hvac_send_connect(_sc) \ miel_hvac_send((_sc), MIEL_HVAC_H_TYPE_CONNECT, \ miel_hvac_msg_connect, sizeof(miel_hvac_msg_connect)) static const struct miel_hvac_map * miel_hvac_map_byname(const char *name, const struct miel_hvac_map *m, size_t n) { const struct miel_hvac_map *e; size_t i; for (i = 0; i < n; i++) { e = &m[i]; if (strcasecmp(e->name, name) == 0) return (e); } return (NULL); } static const char * miel_hvac_map_byval(uint8_t byte, const struct miel_hvac_map *m, size_t n) { const struct miel_hvac_map *e; size_t i; for (i = 0; i < n; i++) { e = &m[i]; if (byte == e->byte) return (e->name); } return (NULL); } static void miel_hvac_request(struct miel_hvac_softc *sc, uint8_t type) { struct miel_hvac_msg_request request = { type }; miel_hvac_send(sc, MIEL_HVAC_H_TYPE_REQUEST, &request, sizeof(request)); } static void miel_hvac_init_update(struct miel_hvac_msg_update *update) { memset(update, 0, sizeof(*update)); update->one = 1; } static inline void miel_hvac_send_update(struct miel_hvac_softc *sc, const struct miel_hvac_msg_update *update) { miel_hvac_send(sc, MIEL_HVAC_H_TYPE_UPDATE, update, sizeof(*update)); } static inline void miel_hvac_send_remotetemp(struct miel_hvac_softc *sc, const struct miel_hvac_msg_remotetemp *remotetemp) { miel_hvac_send(sc, MIEL_HVAC_H_TYPE_UPDATE, remotetemp, sizeof(*remotetemp)); } static bool miel_hvac_set_power(struct miel_hvac_softc *sc) { struct miel_hvac_msg_update *update = &sc->sc_update; uint16_t source = XdrvMailbox.payload; if (source == SRC_SWITCH) return (false); update->flags |= htons(MIEL_HVAC_UPDATE_F_POWER); update->power = (XdrvMailbox.index & (1 << sc->sc_device)) ? MIEL_HVAC_UPDATE_POWER_ON : MIEL_HVAC_UPDATE_POWER_OFF; return (true); } static void miel_hvac_respond_unsupported(void) { ResponseCmndChar_P(PSTR("Unsupported")); } static void miel_hvac_cmnd_setfanspeed(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_update *update = &sc->sc_update; const struct miel_hvac_map *e; if (XdrvMailbox.data_len == 0) return; e = miel_hvac_map_byname(XdrvMailbox.data, miel_hvac_fan_map, nitems(miel_hvac_fan_map)); if (e == NULL) { miel_hvac_respond_unsupported(); return; } update->flags |= htons(MIEL_HVAC_UPDATE_F_FAN); update->fan = e->byte; ResponseCmndChar_P(e->name); } static void miel_hvac_cmnd_setmode(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_update *update = &sc->sc_update; const struct miel_hvac_map *e; if (XdrvMailbox.data_len == 0) return; e = miel_hvac_map_byname(XdrvMailbox.data, miel_hvac_mode_map, nitems(miel_hvac_mode_map)); if (e == NULL) { miel_hvac_respond_unsupported(); return; } update->flags |= htons(MIEL_HVAC_UPDATE_F_MODE); update->mode = e->byte; ResponseCmndChar_P(e->name); } static void miel_hvac_cmnd_sethamode(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_update *update = &sc->sc_update; const struct miel_hvac_map *e; if (XdrvMailbox.data_len == 0) return; if (strcasecmp(XdrvMailbox.data, "off") == 0) { update->flags |= htons(MIEL_HVAC_UPDATE_F_POWER); update->power = MIEL_HVAC_UPDATE_POWER_OFF; ResponseCmndChar_P(PSTR("off")); return; } /* * I could set power and then call miel_hvac_cmnd_setmode, * but that would mean power gets turned on even if there's * an invalid argument. */ e = miel_hvac_map_byname(XdrvMailbox.data, miel_hvac_mode_map, nitems(miel_hvac_mode_map)); if (e == NULL) { miel_hvac_respond_unsupported(); return; } update->flags |= htons(MIEL_HVAC_UPDATE_F_POWER) | htons(MIEL_HVAC_UPDATE_F_MODE); update->power = MIEL_HVAC_UPDATE_POWER_ON; update->mode = e->byte; ResponseCmndChar_P(e->name); } static void miel_hvac_cmnd_settemp(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_update *update = &sc->sc_update; unsigned long degc; if (XdrvMailbox.data_len == 0) return; degc = strtoul(XdrvMailbox.data, nullptr, 0); if (degc < MIEL_HVAC_UPDATE_TEMP_MIN || degc > MIEL_HVAC_UPDATE_TEMP_MAX) { miel_hvac_respond_unsupported(); return; } update->flags |= htons(MIEL_HVAC_UPDATE_F_TEMP); update->temp = miel_hvac_deg2temp(degc); ResponseCmndNumber(degc); } static void miel_hvac_cmnd_setvane(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_update *update = &sc->sc_update; const struct miel_hvac_map *e; if (XdrvMailbox.data_len == 0) return; e = miel_hvac_map_byname(XdrvMailbox.data, miel_hvac_vane_map, nitems(miel_hvac_vane_map)); if (e == NULL) { miel_hvac_respond_unsupported(); return; } update->flags |= htons(MIEL_HVAC_UPDATE_F_VANE); update->vane = e->byte; ResponseCmndChar_P(e->name); } static void miel_hvac_cmnd_setwidevane(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_update *update = &sc->sc_update; const struct miel_hvac_map *e; if (XdrvMailbox.data_len == 0) return; e = miel_hvac_map_byname(XdrvMailbox.data, miel_hvac_widevane_map, nitems(miel_hvac_widevane_map)); if (e == NULL) { miel_hvac_respond_unsupported(); return; } update->flags |= htons(MIEL_HVAC_UPDATE_F_WIDEVANE); update->widevane = e->byte; ResponseCmndChar_P(e->name); } static inline uint8_t miel_hvac_remotetemp_degc2old(long degc) { /* * If a remote temperature reading is provided that cannot be * represented by the temp_old field, implicitly clamp it to the * supported min or max. The hardware does this anyway if you * provide a high value, but without this the min value will * underflow and turn a high value that the hardware thinks is 38. */ if (degc < MIEL_HVAC_REMOTETEMP_OLD_MIN) degc = MIEL_HVAC_REMOTETEMP_OLD_MIN; else if (degc > MIEL_HVAC_REMOTETEMP_OLD_MAX) degc = MIEL_HVAC_REMOTETEMP_OLD_MIN; return ((degc - MIEL_HVAC_REMOTETEMP_OLD_MIN) * MIEL_HVAC_REMOTETEMP_OLD_FACTOR); } static void miel_hvac_cmnd_remotetemp(void) { struct miel_hvac_softc *sc = miel_hvac_sc; struct miel_hvac_msg_remotetemp *rt = &sc->sc_remotetemp; uint8_t control = MIEL_HVAC_REMOTETEMP_SET; long degc; if (XdrvMailbox.data_len == 0) return; if (strcasecmp(XdrvMailbox.data, "clear") == 0) { control = MIEL_HVAC_REMOTETEMP_CLR; degc = 0; ResponseCmndChar_P("clear"); } else { degc = strtol(XdrvMailbox.data, nullptr, 0); /* clamp the argument to supported values */ if (degc < MIEL_HVAC_REMOTETEMP_MIN) degc = MIEL_HVAC_REMOTETEMP_MIN; else if (degc > MIEL_HVAC_REMOTETEMP_MAX) degc = MIEL_HVAC_REMOTETEMP_MAX; ResponseCmndNumber(degc); } memset(rt, 0, sizeof(*rt)); rt->seven = 0x7; rt->control = control; /* * Different HVACs (or more likely different generations * of these HVACs) have different ways to encode the remote * temperature value. This provides both of them to hopefully * support all known types of HVACs. */ rt->temp_old = miel_hvac_remotetemp_degc2old(degc); rt->temp = (degc + MIEL_HVAC_REMOTETEMP_OFFSET) * MIEL_HVAC_REMOTETEMP_OLD_FACTOR; } #ifdef MIEL_HVAC_DEBUG static void miel_hvac_cmnd_request(void) { struct miel_hvac_softc *sc = miel_hvac_sc; uint8_t type = MIEL_HVAC_REQUEST_ROOMTEMP; if (XdrvMailbox.data_len > 0) type = strtoul(XdrvMailbox.data, nullptr, 0); miel_hvac_request(sc, type); ResponseCmndDone(); } #endif /* * serial data handlers */ static void miel_hvac_log_bytes(struct miel_hvac_softc *sc, const char *name, const void *buf, size_t len) { char hex[(MIEL_HVAC_DATABUFLEN + 1) * 2]; const unsigned char *b = (const unsigned char *)buf; AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": response %s %s"), name, ToHex_P(b, len, hex, sizeof(hex))); } static void miel_hvac_input_connected(struct miel_hvac_softc *sc, const void *buf, size_t len) { AddLog_P2(LOG_LEVEL_INFO, PSTR(MIEL_HVAC_LOGNAME ": connected to Mitsubishi Electric HVAC")); sc->sc_connected = 1; } static void miel_hvac_publish_settings(struct miel_hvac_softc *sc) { const struct miel_hvac_data_settings *set = &sc->sc_settings.data.settings; char hex[(sizeof(sc->sc_settings) + 1) * 2]; char temp[33]; const char *name; Response_P(PSTR("{\"" D_JSON_IRHVAC_POWER "\":\"%s\""), set->power ? "ON" : "OFF"); name = miel_hvac_map_byval( set->mode & MIEL_HVAC_SETTINGS_MODE_MASK, miel_hvac_mode_map, nitems(miel_hvac_mode_map)); if (name != NULL) { ResponseAppend_P(PSTR(",\"" D_JSON_IRHVAC_MODE "\":\"%s\""), name); ResponseAppend_P(PSTR(",\"HA" D_JSON_IRHVAC_MODE "\":\"%s\""), set->power ? name : "off"); } dtostrfd(ConvertTemp(miel_hvac_temp2deg(set->temp)), Settings.flag2.temperature_resolution, temp); ResponseAppend_P(PSTR(",\"" D_JSON_IRHVAC_TEMP "\":%s"), temp); name = miel_hvac_map_byval(set->fan, miel_hvac_fan_map, nitems(miel_hvac_fan_map)); if (name != NULL) { ResponseAppend_P(PSTR(",\"" D_JSON_IRHVAC_FANSPEED "\":\"%s\""), name); } name = miel_hvac_map_byval(set->vane, miel_hvac_vane_map, nitems(miel_hvac_vane_map)); if (name != NULL) { ResponseAppend_P(PSTR(",\"" D_JSON_IRHVAC_SWINGV "\":\"%s\""), name); } name = miel_hvac_map_byval(set->widevane & MIEL_HVAC_SETTTINGS_WIDEVANE_MASK, miel_hvac_widevane_map, nitems(miel_hvac_widevane_map)); if (name != NULL) { ResponseAppend_P(PSTR(",\"" D_JSON_IRHVAC_SWINGH "\":\"%s\""), name); } ResponseAppend_P(PSTR(",\"Bytes\":\"%s\""), ToHex_P((uint8_t *)&sc->sc_settings, sizeof(sc->sc_settings), hex, sizeof(hex))); ResponseAppend_P(PSTR("}")); MqttPublishPrefixTopic_P(TELE, PSTR("HVACSettings")); XdrvRulesProcess(); } static void miel_hvac_input_settings(struct miel_hvac_softc *sc, const struct miel_hvac_data *d) { const struct miel_hvac_data_settings *set = &d->data.settings; uint32_t state = set->power ? 1 : 0; /* see ExecuteCommandPower */ bool publish; if (miel_hvac_update_pending(sc)) { /* * Don't flop around printing settings that we might be * changing, but also force them to be published again. */ sc->sc_settings_set = 0; return; } if (bitRead(TasmotaGlobal.power, sc->sc_device) != !!state) ExecuteCommandPower(sc->sc_device, state, SRC_SWITCH); publish = (sc->sc_settings_set == 0) || (memcmp(d, &sc->sc_settings, sizeof(sc->sc_settings)) != 0); sc->sc_settings_set = 1; sc->sc_settings = *d; if (publish) miel_hvac_publish_settings(sc); } static void miel_hvac_data_response(struct miel_hvac_softc *sc, const struct miel_hvac_data *d) { char hex[(sizeof(*d) + 1) * 2]; Response_P(PSTR("{\"Bytes\":\"%s\"}"), ToHex_P((uint8_t *)d, sizeof(*d), hex, sizeof(hex))); MqttPublishPrefixTopic_P(TELE, PSTR("HVACData")); XdrvRulesProcess(); } static void miel_hvac_input_sensor(struct miel_hvac_softc *sc, struct miel_hvac_data *dst, const struct miel_hvac_data *src) { bool publish; publish = (memcmp(dst, src, sizeof(*dst)) != 0); *dst = *src; if (publish) MqttPublishSensor(); } static void miel_hvac_input_data(struct miel_hvac_softc *sc, const void *buf, size_t len) { const struct miel_hvac_data *d; miel_hvac_log_bytes(sc, "data", buf, len); if (len < sizeof(*d)) { AddLog_P2(LOG_LEVEL_DEBUG, PSTR(MIEL_HVAC_LOGNAME ": short data response (%zu < %zu)"), len, sizeof(*d)); return; } d = (const struct miel_hvac_data *)buf; switch (d->type) { case MIEL_HVAC_DATA_T_SETTINGS: miel_hvac_input_settings(sc, d); break; case MIEL_HVAC_DATA_T_ROOMTEMP: miel_hvac_input_sensor(sc, &sc->sc_temp, d); break; case MIEL_HVAC_DATA_T_STATUS: miel_hvac_input_sensor(sc, &sc->sc_status, d); break; case MIEL_HVAC_DATA_T_STAGE: miel_hvac_input_sensor(sc, &sc->sc_stage, d); break; default: miel_hvac_data_response(sc, d); break; } } static void miel_hvac_input_updated(struct miel_hvac_softc *sc, const void *buf, size_t len) { miel_hvac_log_bytes(sc, "updated", buf, len); } /* * FUNC handlers */ static void miel_hvac_pre_init(void) { struct miel_hvac_softc *sc; int baudrate = 2400; if (!PinUsed(GPIO_MIEL_HVAC_TX) || !PinUsed(GPIO_MIEL_HVAC_RX)) return; sc = (struct miel_hvac_softc *)malloc(sizeof(*sc)); if (sc == NULL) { AddLog_P(LOG_LEVEL_ERROR, PSTR(MIEL_HVAC_LOGNAME ": unable to allocate state")); return; } memset(sc, 0, sizeof(*sc)); miel_hvac_init_update(&sc->sc_update); sc->sc_serial = new TasmotaSerial(Pin(GPIO_MIEL_HVAC_RX), Pin(GPIO_MIEL_HVAC_TX), 2); if (!sc->sc_serial->begin(baudrate, 2)) { AddLog_P2(LOG_LEVEL_ERROR, PSTR(MIEL_HVAC_LOGNAME ": unable to begin serial " "(baudrate %d)"), baudrate); goto del; } if (sc->sc_serial->hardwareSerial()) { ClaimSerial(); SetSerial(baudrate, TS_SERIAL_8E1); } sc->sc_device = TasmotaGlobal.devices_present++; /* claim a POWER device slot */ miel_hvac_sc = sc; return; del: delete sc->sc_serial; free: free(sc); } static void miel_hvac_loop(struct miel_hvac_softc *sc) { TasmotaSerial *serial = sc->sc_serial; while (serial->available()) { yield(); sc->sc_parser.p_state = miel_hvac_parse(sc, serial->read()); } } static void miel_hvac_sensor(struct miel_hvac_softc *sc) { char hex[(sizeof(sc->sc_status) + 1) * 2]; const char *sep = ""; ResponseAppend_P(PSTR("," "\"MiElHVAC\":{")); if (sc->sc_temp.type != 0) { const struct miel_hvac_data_roomtemp *rt = &sc->sc_temp.data.roomtemp; unsigned int temp = miel_hvac_roomtemp2deg(rt->temp); char room_temp[33]; dtostrfd(ConvertTemp(temp), Settings.flag2.temperature_resolution, room_temp); ResponseAppend_P(PSTR("\"" D_JSON_TEMPERATURE "\":%s"), room_temp); sep = ","; } if (sc->sc_status.type != 0) { const struct miel_hvac_data_status *s = &sc->sc_status.data.status; ResponseAppend_P(PSTR("%s" "\"Operation\":\"%s\"" "," "\"Compressor\":\"%s\""), sep, s->operation ? "ON" : "OFF", s->compressor ? "ON" : "OFF"); sep = ","; } if (sc->sc_temp.type != 0) { ResponseAppend_P(PSTR("%s" "\"roomtemp\":\"%s\""), sep, ToHex_P((uint8_t *)&sc->sc_temp, sizeof(sc->sc_temp), hex, sizeof(hex))); sep = ","; } if (sc->sc_status.type != 0) { ResponseAppend_P(PSTR("%s" "\"status\":\"%s\""), sep, ToHex_P((uint8_t *)&sc->sc_status, sizeof(sc->sc_status), hex, sizeof(hex))); sep = ","; } if (sc->sc_stage.type != 0) { ResponseAppend_P(PSTR("%s" "\"stage\":\"%s\""), sep, ToHex_P((uint8_t *)&sc->sc_stage, sizeof(sc->sc_stage), hex, sizeof(hex))); } ResponseAppend_P(PSTR("}")); } /* * This is set up to pace interactions with the aircon so we should * only have a single outstanding request at a time, and to avoid trying * to change settings while in the middle of reading them. SETTINGS is * requested frequently so changes from the IR remote are noticed quickly * and published. Posting new settings preempts queries for information. */ enum miel_hvac_connect_states { MIEL_HVAC_CONNECT_S_2400_MSG, MIEL_HVAC_CONNECT_S_9600, MIEL_HVAC_CONNECT_S_9600_MSG, MIEL_HVAC_CONNECT_S_2400, MIEL_HVAC_CONNECT_S_COUNT, }; static void miel_hvac_connect(struct miel_hvac_softc *sc) { TasmotaSerial *serial = sc->sc_serial; uint32_t baudrate; unsigned int state; state = (sc->sc_tick++ % MIEL_HVAC_CONNECT_S_COUNT); switch (state) { case MIEL_HVAC_CONNECT_S_2400: baudrate = 2400; break; case MIEL_HVAC_CONNECT_S_9600: baudrate = 9600; break; default: miel_hvac_send_connect(sc); return; } serial->begin(baudrate, 2); if (serial->hardwareSerial()) SetSerial(baudrate, TS_SERIAL_8E1); } static void miel_hvac_tick(struct miel_hvac_softc *sc) { static const uint8_t updates[] = { MIEL_HVAC_REQUEST_SETTINGS, MIEL_HVAC_REQUEST_STATUS, MIEL_HVAC_REQUEST_SETTINGS, MIEL_HVAC_REQUEST_ROOMTEMP, MIEL_HVAC_REQUEST_SETTINGS, /* MUZ-GA80VA doesnt respond :( */ MIEL_HVAC_REQUEST_STAGE, }; unsigned int i; if (miel_hvac_update_pending(sc)) { struct miel_hvac_msg_update *update = &sc->sc_update; miel_hvac_send_update(sc, update); miel_hvac_init_update(update); /* refresh settings on next tick */ sc->sc_tick = 0; return; } if (sc->sc_remotetemp.seven) { struct miel_hvac_msg_remotetemp *remotetemp = &sc->sc_remotetemp; miel_hvac_send_remotetemp(sc, remotetemp); memset(remotetemp, 0, sizeof(*remotetemp)); return; } i = (sc->sc_tick++ % nitems(updates)); miel_hvac_request(sc, updates[i]); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ static const char miel_hvac_cmnd_names[] PROGMEM = // No prefix "|" D_CMND_MIEL_HVAC_SETFANSPEED "|" D_CMND_MIEL_HVAC_SETMODE "|" D_CMND_MIEL_HVAC_SETHAMODE "|" D_CMND_MIEL_HVAC_SETTEMP "|" D_CMND_MIEL_HVAC_SETSWINGV "|" D_CMND_MIEL_HVAC_SETSWINGH "|" D_CMND_MIEL_HVAC_REMOTETEMP #ifdef MIEL_HVAC_DEBUG "|" "HVACRequest" #endif ; static void (*const miel_hvac_cmnds[])(void) PROGMEM = { &miel_hvac_cmnd_setfanspeed, &miel_hvac_cmnd_setmode, &miel_hvac_cmnd_sethamode, /* rain of hate */ &miel_hvac_cmnd_settemp, &miel_hvac_cmnd_setvane, &miel_hvac_cmnd_setwidevane, &miel_hvac_cmnd_remotetemp, #ifdef MIEL_HVAC_DEBUG &miel_hvac_cmnd_request, #endif }; bool Xdrv44(uint8_t function) { bool result = false; struct miel_hvac_softc *sc = miel_hvac_sc; switch (function) { case FUNC_PRE_INIT: miel_hvac_pre_init(); return (false); } if (sc == NULL) return (false); switch (function) { case FUNC_LOOP: miel_hvac_loop(sc); break; case FUNC_SET_DEVICE_POWER: result = miel_hvac_set_power(sc); break; case FUNC_EVERY_250_MSECOND: if (sc->sc_connected) miel_hvac_tick(sc); else miel_hvac_connect(sc); break; case FUNC_EVERY_50_MSECOND: case FUNC_EVERY_100_MSECOND: case FUNC_EVERY_200_MSECOND: case FUNC_EVERY_SECOND: break; case FUNC_JSON_APPEND: miel_hvac_sensor(sc); break; case FUNC_AFTER_TELEPERIOD: if (sc->sc_settings_set) miel_hvac_publish_settings(sc); break; case FUNC_COMMAND: result = DecodeCommand(miel_hvac_cmnd_names, miel_hvac_cmnds); break; } return (result); } #endif // USE_MIEL_HVAC