diff --git a/README.md b/README.md index 2fc57ea..8380a6d 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,28 @@ Wirelessly control your Mitsubishi Comfort HVAC equipment with an ESP8266 or ESP32 using the [ESPHome](https://esphome.io) framework. ## Features + * Instant feedback of command changes via RF Remote to HomeAssistant or MQTT. * Direct control without the remote. * Uses the [SwiCago/HeatPump](https://github.com/SwiCago/HeatPump) Arduino libary to talk to the unit directly via the internal `CN105` connector. ## Requirements -* https://github.com/SwiCago/HeatPump + +* [SwiCago/HeatPump](https://github.com/SwiCago/HeatPump) * ESPHome 1.19.1 or greater ## Supported Microcontrollers + This library should work on most ESP8266 or ESP32 platforms. It has been tested with the following MCUs: + * Generic ESP-01S board (ESP8266) * WeMos D1 Mini (ESP8266) * Generic ESP32 Dev Kit (ESP32) ## Supported Mitsubishi Climate Units + The underlying HeatPump library works with a number of Mitsubishi HVAC units. Basically, if the unit has a `CN105` header on the main board, it should work with this library. The [HeatPump @@ -34,11 +39,13 @@ available. The whole integration with this libary and the underlying HeatPump has been tested by the author on the following units: + * `MSZ-GL06NA` * `MFZ-KA09NA` ## Usage -### Step 1: Build a control circuit. + +### Step 1: Build a control circuit Build a control circuit with your MCU as detailed in the [SwiCago/HeatPump README](https://github.com/SwiCago/HeatPump/blob/master/README.md#demo-circuit). @@ -70,6 +77,7 @@ external_components: Version 2.0 and greater of this libary use the ESPHome `external_components` feature, which is a huge step forward in terms of usability. In order to make things compile correctly, you will need to: + 1. Remove the `libraries` section that imports `https://github.com/SwiCago/HeatPump`, as this is handled by the `external_component` section of manifest. @@ -81,7 +89,7 @@ things compile correctly, you will need to: 5. You may also have to delete the _esphomenodename_ directory that corresponds with your _esphomenodename.yaml_ configuration file completely. This directory may exist in your base config directory, - or in `config/.esphome/build`. Testing with ESPHome 0.18.x showed this + or in `config/.esphome/build`. Testing with ESPHome 0.18.x showed this to be necessary to get the cached copy of src/esphome-mitsubishiheatpump to go away entirely, as the "Clean Build Files" isn't as thorough as one would like. @@ -90,7 +98,8 @@ and `libraries` lines will likely result in compilation errors complaining about duplicate declarations of `MitsubishiHeatPump::traits()`. ##### Example error -``` + +```none Linking /data/bedroom_east_heatpump/.pioenvs/bedroom_east_heatpump/firmware.elf /root/.platformio/packages/toolchain-xtensa/bin/../lib/gcc/xtensa-lx106-elf/4.8.2/../../../../xtensa-lx106-elf/bin/ld: /data/bedroom_east_heatpump/.pioenvs/bedroom_east_heatpump/src/esphome/components/mitsubishi_heatpump/espmhp.cpp.o: in function `MitsubishiHeatPump::traits()': espmhp.cpp:(.text._ZN18MitsubishiHeatPump6traitsEv+0x4): multiple definition of `MitsubishiHeatPump::traits()'; /data/bedroom_east_heatpump/.pioenvs/bedroom_east_heatpump/src/esphome-mitsubishiheatpump/espmhp.cpp.o:espmhp.cpp:(.text._ZN18MitsubishiHeatPump6traitsEv+0x80): first defined here @@ -139,14 +148,14 @@ software serial libraries, including the one in ESPHome. There's currently no way to guarantee access to a hardware UART nor retrieve the `HardwareSerial` handle from the `uart` component within the ESPHome framework. -# Example configurations +## Example configurations Below is an example configuration which will include wireless strength indicators and permit over the air updates. You'll need to create a `secrets.yaml` file inside of your `esphome` directory with entries for the various items prefixed with `!secret`. -## ESP8266 Example Configuration +### ESP8266 Example Configuration ```yaml substitutions: @@ -239,7 +248,7 @@ climate: baud_rate: 4800 ``` -## ESP32 Example Configuration +### ESP32 Example Configuration ```yaml substitutions: @@ -324,7 +333,7 @@ climate: hardware_uart: UART1 ``` -# Advanced configuration +### Advanced configuration Some models of heat pump require different baud rates or don't support all possible modes of operation. You can configure mulitple climate "traits" in @@ -351,40 +360,54 @@ climate: ## Configuration variables that affect this library directly -* *hardware\_uart* (_Optional_): the hardware UART instance to use for +* `hardware_uart` (_Optional_): the hardware UART instance to use for communcation with the heatpump. On ESP8266, only `UART0` is usable. On ESP32, `UART0`, `UART1`, and `UART2` are all valid choices. Default: `UART0` -* *baud\_rate* (_Optional_): Serial baud rate used to communicate with the +* `baud_rate` (_Optional_): Serial BAUD rate used to communicate with the HeatPump. Most systems use the default value of `4800` baud, but some use `2400` or `9600`. Check [here](https://github.com/SwiCago/HeatPump/issues/13) to find discussion of whether your particular model requires a non-default baud rate. - Some ESP32 boards will require the baud_rate setting if + Some ESP32 boards will require the baud_rate setting if hardware_uart is specified. Default: `4800`. -* *rx\_pin* (_Optional_): pin number to use as RX for the specified hardware +* `rx_pin` (_Optional_): pin number to use as RX for the specified hardware UART (ESP32 only - ESP8266 hardware UART's pins aren't configurable). -* *tx\_pin* (_Optional_): pin number to use as TX for the specified hardware +* `tx_pin` (_Optional_): pin number to use as TX for the specified hardware UART (ESP32 only - ESP8266 hardware UART's pins aren't configurable). -* *update\_interval* (_Optional_, range: 0ms to 9000ms): How often this +* `update_interval` (_Optional_, range: 0ms to 9000ms): How often this component polls the heatpump hardware, in milliseconds. Maximum usable value is 9 seconds due to underlying issues with the HeatPump library. Default: 500ms -* *supports* (_Optional_): Supported features for the device. - * *mode* (_Optional_, list): Supported climate modes for the HeatPump. Default: +* `supports` (_Optional_): Supported features for the device. + ** `mode` (_Optional_, list): Supported climate modes for the HeatPump. Default: `['HEAT_COOL', 'COOL', 'HEAT', 'DRY', 'FAN_ONLY']` - * *fan_mode* (_Optional_, list): Supported fan speeds for the HeatPump. + ** `fan_mode` (_Optional_, list): Supported fan speeds for the HeatPump. Default: `['AUTO', 'DIFFUSE', 'LOW', 'MEDIUM', 'MIDDLE', 'HIGH']` - * *swing_mode* (_Optional_, list): Supported fan swing modes. Most Mitsubishi + ** `swing_mode` (_Optional_, list): Supported fan swing modes. Most Mitsubishi units only support the default. Default: `['OFF', 'VERTICAL']` +* `remote_temperature_operating_timeout_minutes` (_Optional_): The number of + minutes before a set_remote_temperature request becomes stale, while the + heatpump is heating or cooling. Unless a new set_remote_temperature + request was made within the time duration, the heatpump will revert back to it's + internal temperature sensor. +* `remote_temperature_idle_timeout_minutes` (_Optional_): The number of + minutes before a set_remote_temperature request becomes stale while the heatpump + is idle. Unless a new set_remote_temperature request is made within the time duration, + the heatpump will revert back to it's internal temperature sensor. +* `remote_temperature_ping_timeout_minutes` (_Optional_): The number of + minutes before a set_remote_temperature request becomes stale, if a ping + request wasn't received from your ESPHome controller. This will result + in the heatpump reverting to it's internal temperature sensor if the heatpump + loses it's WiFi connection. ## Other configuration -* *id* (_Optional_): used to identify multiple instances, e.g. "denheatpump" -* *name* (_Required_): The name of the climate component, e.g. "Den Heatpump" -* *visual* (_Optional_): The core `Climate` component has several *visual* +* `id` (_Optional_): used to identify multiple instances, e.g. "denheatpump" +* `name` (_Required_): The name of the climate component, e.g. "Den Heatpump" +* `visual` (_Optional_): The core `Climate` component has several *visual* options that can be set. See the [Climate Component](https://esphome.io/components/climate/index.html) documentation for details. -## Remote temperature +### Remote temperature It is possible to use an external temperature sensor to tell the heat pump what the room temperature is, rather than relying on its internal temperature @@ -447,9 +470,44 @@ api: - lambda: 'id(hp).set_remote_temperature(0);' ``` -# See Also +It's also possible to configure timeouts which will revert the heatpump +back to it's internal temperature sensor in the event that an external sensor +becomes unavailable. All three settings are optional, but it's recommended +that you configure both operating and idle timeout. Both can be configured to the same +value. + +```yaml +climate: + - platform: mitsubishi_heatpump + remote_temperature_operating_timeout_minutes: 65 + remote_temperature_idle_timeout_minutes: 120 + remote_temperature_ping_timeout_minutes: 20 + +api: + services: + - service: ping + then: + - lambda: 'id(hp).ping();' +``` + +There is an explicit distinction between an operating timeout and an idle timeout. + +* **Operating timeout** The heatpump is currently pumping heat, and the expectation is that + the temperature should shift within a certain time period. Recommended value: 60 minutes. +* **Idle timeout** The heatpump is not currently pumping heat, so temperature shifts are expected + to happen less frequently. Recommended value depends on the implementation details of your temperature + sensor. Some will only provide updates on temperature changes, others such as Aqara will provide + an update at least once every hour. +* **Ping timeout** Detects if a connection is lost between HomeAssistant and the heatpump, or if your + home assistant instance is down. Recommended value is 20 minutes, with a ping being sent every 5 minutes. + +Do not enable ping timeout until you have the logic in place to call the ping service at a regular interval. You +can view the ESPHome logs to ensure this is taking place. + +## See Also + +### Other Implementations -## Other Implementations The [gysmo38/mitsubishi2MQTT](https://github.com/gysmo38/mitsubishi2MQTT) Arduino sketch also uses the `SwiCago/HeatPump` library, and works with MQTT directly. The author of this implementation found @@ -466,9 +524,10 @@ repository and it's underlying `HeatPump` library allows bi-directional communication with the Mitsubishi system, and can detect when someone changes the settings via an IR remote. -## Reference documentation +### Reference documentation The author referred to the following documentation repeatedly: + * [ESPHome Custom Sensors Reference](https://esphome.io/components/sensor/custom.html) * [ESPHome Custom Climate Components Reference](https://esphome.io/components/climate/custom.html) * [ESPHome External Components Reference](https://esphome.io/components/external_components.html) diff --git a/components/mitsubishi_heatpump/climate.py b/components/mitsubishi_heatpump/climate.py index 5fd7f31..371e421 100644 --- a/components/mitsubishi_heatpump/climate.py +++ b/components/mitsubishi_heatpump/climate.py @@ -22,6 +22,11 @@ DEFAULT_CLIMATE_MODES = ["HEAT_COOL", "COOL", "HEAT", "DRY", "FAN_ONLY"] DEFAULT_FAN_MODES = ["AUTO", "DIFFUSE", "LOW", "MEDIUM", "MIDDLE", "HIGH"] DEFAULT_SWING_MODES = ["OFF", "VERTICAL"] +# Remote temperature timeout configuration +CONF_REMOTE_OPERATING_TIMEOUT = "remote_temperature_operating_timeout_minutes" +CONF_REMOTE_IDLE_TIMEOUT = "remote_temperature_idle_timeout_minutes" +CONF_REMOTE_PING_TIMEOUT = "remote_temperature_ping_timeout_minutes" + MitsubishiHeatPump = cg.global_ns.class_( "MitsubishiHeatPump", climate.Climate, cg.PollingComponent ) @@ -43,6 +48,9 @@ CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( cv.GenerateID(): cv.declare_id(MitsubishiHeatPump), cv.Optional(CONF_HARDWARE_UART, default="UART0"): valid_uart, cv.Optional(CONF_BAUD_RATE): cv.positive_int, + cv.Optional(CONF_REMOTE_OPERATING_TIMEOUT): cv.positive_int, + cv.Optional(CONF_REMOTE_IDLE_TIMEOUT): cv.positive_int, + cv.Optional(CONF_REMOTE_PING_TIMEOUT): cv.positive_int, cv.Optional(CONF_RX_PIN): cv.positive_int, cv.Optional(CONF_TX_PIN): cv.positive_int, # If polling interval is greater than 9 seconds, the HeatPump library @@ -79,6 +87,16 @@ def to_code(config): if CONF_TX_PIN in config: cg.add(var.set_tx_pin(config[CONF_TX_PIN])) + if CONF_REMOTE_OPERATING_TIMEOUT in config: + cg.add(var.set_remote_operating_timeout_minutes(config[CONF_REMOTE_OPERATING_TIMEOUT])) + + if CONF_REMOTE_IDLE_TIMEOUT in config: + cg.add(var.set_remote_idle_timeout_minutes(config[CONF_REMOTE_IDLE_TIMEOUT])) + + if CONF_REMOTE_PING_TIMEOUT in config: + cg.add(var.set_remote_ping_timeout_minutes(config[CONF_REMOTE_PING_TIMEOUT])) + + supports = config[CONF_SUPPORTS] traits = var.config_traits() diff --git a/components/mitsubishi_heatpump/espmhp.cpp b/components/mitsubishi_heatpump/espmhp.cpp index 832462d..82cf416 100644 --- a/components/mitsubishi_heatpump/espmhp.cpp +++ b/components/mitsubishi_heatpump/espmhp.cpp @@ -9,7 +9,8 @@ * Author: @am-io on Github. * Author: @nao-pon on Github. * Author: Simon Knopp @sijk on Github - * Last Updated: 2021-05-27 + * Author: Paul Murphy @donutsoft on GitHub + * Last Updated: 2023-04-22 * License: BSD * * Requirements: @@ -40,6 +41,10 @@ MitsubishiHeatPump::MitsubishiHeatPump( this->traits_.set_visual_min_temperature(ESPMHP_MIN_TEMPERATURE); this->traits_.set_visual_max_temperature(ESPMHP_MAX_TEMPERATURE); this->traits_.set_visual_temperature_step(ESPMHP_TEMPERATURE_STEP); + + // Assume a succesful connection was made to the ESPHome controller on + // launch. + this->ping(); } void MitsubishiHeatPump::check_logger_conflict_() { @@ -67,6 +72,7 @@ void MitsubishiHeatPump::update() { heatpumpStatus currentStatus = hp->getStatus(); this->hpStatusChanged(currentStatus); #endif + this->enforce_remote_temperature_sensor_timeout(); } void MitsubishiHeatPump::set_baud_rate(int baud) { @@ -119,6 +125,24 @@ void MitsubishiHeatPump::control(const climate::ClimateCall &call) { if (has_mode){ this->mode = *call.get_mode(); } + + if (last_remote_temperature_sensor_update_.has_value()) { + // Some remote temperature sensors will only issue updates when a change + // in temperature occurs. + + // Assume a case where the idle sensor timeout is 12hrs and operating + // timeout is 1hr. If the user changes the HP setpoint after 1.5hrs, the + // machine will switch to operating mode, the remote temperature + // reading will expire and the HP will revert to it's internal + // temperature sensor. + + // This change ensures that if the user changes the machine setpoint, + // the remote sensor has an opportunity to issue an update to reflect + // the new change in temperature. + last_remote_temperature_sensor_update_ = + std::chrono::steady_clock::now(); + } + switch (this->mode) { case climate::CLIMATE_MODE_COOL: hp->setModeSetting("COOL"); @@ -195,7 +219,7 @@ void MitsubishiHeatPump::control(const climate::ClimateCall &call) { //const char* FAN_MAP[6] = {"AUTO", "QUIET", "1", "2", "3", "4"}; if (call.get_fan_mode().has_value()) { - ESP_LOGV("control", "Requested fan mode is %s", *call.get_fan_mode()); + ESP_LOGV("control", "Requested fan mode is %d", *call.get_fan_mode()); this->fan_mode = *call.get_fan_mode(); switch(*call.get_fan_mode()) { case climate::CLIMATE_FAN_OFF: @@ -233,7 +257,7 @@ void MitsubishiHeatPump::control(const climate::ClimateCall &call) { //const char* VANE_MAP[7] = {"AUTO", "1", "2", "3", "4", "5", "SWING"}; if (call.get_swing_mode().has_value()) { - ESP_LOGV(TAG, "control - requested swing mode is %s", + ESP_LOGD(TAG, "control - requested swing mode is %d", *call.get_swing_mode()); this->swing_mode = *call.get_swing_mode(); @@ -341,7 +365,7 @@ void MitsubishiHeatPump::hpSettingsChanged() { } else { //case "AUTO" or default: this->fan_mode = climate::CLIMATE_FAN_AUTO; } - ESP_LOGI(TAG, "Fan mode is: %i", this->fan_mode); + ESP_LOGI(TAG, "Fan mode is: %i", this->fan_mode.value_or(-1)); /* ******** HANDLE MITSUBISHI VANE CHANGES ******** * const char* VANE_MAP[7] = {"AUTO", "1", "2", "3", "4", "5", "SWING"}; @@ -415,14 +439,71 @@ void MitsubishiHeatPump::hpStatusChanged(heatpumpStatus currentStatus) { this->action = climate::CLIMATE_ACTION_OFF; } + this->operating_ = currentStatus.operating; + this->publish_state(); } void MitsubishiHeatPump::set_remote_temperature(float temp) { ESP_LOGD(TAG, "Setting remote temp: %.1f", temp); + if (temp > 0) { + last_remote_temperature_sensor_update_ = + std::chrono::steady_clock::now(); + } else { + last_remote_temperature_sensor_update_.reset(); + } + this->hp->setRemoteTemperature(temp); } +void MitsubishiHeatPump::ping() { + ESP_LOGD(TAG, "Ping request received"); + last_ping_request_ = std::chrono::steady_clock::now(); +} + +void MitsubishiHeatPump::set_remote_operating_timeout_minutes(int minutes) { + ESP_LOGD(TAG, "Setting remote operating timeout time: %d minutes", minutes); + remote_operating_timeout_ = std::chrono::minutes(minutes); +} + +void MitsubishiHeatPump::set_remote_idle_timeout_minutes(int minutes) { + ESP_LOGD(TAG, "Setting remote idle timeout time: %d minutes", minutes); + remote_idle_timeout_ = std::chrono::minutes(minutes); +} + +void MitsubishiHeatPump::set_remote_ping_timeout_minutes(int minutes) { + ESP_LOGD(TAG, "Setting remote ping timeout time: %d minutes", minutes); + remote_ping_timeout_ = std::chrono::minutes(minutes); +} + +void MitsubishiHeatPump::enforce_remote_temperature_sensor_timeout() { + // Handle ping timeouts. + if (remote_ping_timeout_.has_value() && last_ping_request_.has_value()) { + auto time_since_last_ping = + std::chrono::steady_clock::now() - last_ping_request_.value(); + if(time_since_last_ping > remote_ping_timeout_.value()) { + ESP_LOGW(TAG, "Ping timeout."); + this->set_remote_temperature(0); + last_ping_request_.reset(); + return; + } + } + + // Handle set_remote_temperature timeouts. + auto remote_set_temperature_timeout = + this->operating_ ? remote_operating_timeout_ : remote_idle_timeout_; + if (remote_set_temperature_timeout.has_value() && + last_remote_temperature_sensor_update_.has_value()) { + auto time_since_last_temperature_update = + std::chrono::steady_clock::now() - last_remote_temperature_sensor_update_.value(); + if (time_since_last_temperature_update > remote_set_temperature_timeout.value()) { + ESP_LOGW(TAG, "Set remote temperature timeout, operating=%d", this->operating_); + this->set_remote_temperature(0); + return; + } + } +} + void MitsubishiHeatPump::setup() { // This will be called by App.setup() this->banner(); diff --git a/components/mitsubishi_heatpump/espmhp.h b/components/mitsubishi_heatpump/espmhp.h index 1e5f62d..b7598c6 100644 --- a/components/mitsubishi_heatpump/espmhp.h +++ b/components/mitsubishi_heatpump/espmhp.h @@ -19,6 +19,7 @@ #include "esphome.h" #include "esphome/core/preferences.h" +#include #include "HeatPump.h" @@ -99,6 +100,22 @@ class MitsubishiHeatPump : public esphome::PollingComponent, public esphome::cli // set_remote_temp(0) to switch back to the internal sensor. void set_remote_temperature(float); + // Used to validate that a connection is present between the controller + // and this heatpump. + void ping(); + + // Number of minutes before the heatpump reverts back to the internal + // temperature sensor if the machine is currently operating. + void set_remote_operating_timeout_minutes(int); + + // Number of minutes before the heatpump reverts back to the internal + // temperature sensor if the machine is currently idle. + void set_remote_idle_timeout_minutes(int); + + // Number of minutes before the heatpump reverts back to the internal + // temperature sensor if a ping isn't received from the controller. + void set_remote_ping_timeout_minutes(int); + protected: // HeatPump object using the underlying Arduino library. HeatPump* hp; @@ -132,11 +149,20 @@ class MitsubishiHeatPump : public esphome::PollingComponent, public esphome::cli static esphome::optional load(esphome::ESPPreferenceObject& storage); private: + void enforce_remote_temperature_sensor_timeout(); + // Retrieve the HardwareSerial pointer from friend and subclasses. HardwareSerial *hw_serial_; int baud_ = 0; int rx_pin_ = -1; int tx_pin_ = -1; + bool operating_ = false; + + std::optional>> remote_operating_timeout_; + std::optional>> remote_idle_timeout_; + std::optional>> remote_ping_timeout_; + std::optional> last_remote_temperature_sensor_update_; + std::optional> last_ping_request_; }; #endif