diff --git a/.gitignore b/.gitignore index 259148f..9c89171 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ *.exe *.out *.app + +# Python cache +__pycache__ diff --git a/README.md b/README.md index 7d7e80d..2375d98 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ESP32 using the [ESPHome](https://esphome.io) framework. ## Requirements * https://github.com/SwiCago/HeatPump -* ESPHome 1.15.0 or greater +* ESPHome 1.18.0 or greater ## Supported Microcontrollers This library should work on most ESP8266 or ESP32 platforms. It has been tested @@ -50,53 +50,47 @@ to the control board](https://github.com/SwiCago/HeatPump/issues/13#issuecomment-457897457) via CN105. -### Step 2: Use ESPHome 1.15.0 or higher +### Step 2: Use ESPHome 1.18.0 or higher -The code in this repository makes use of a number of features in the 1.15.0 version of ESPHome, including various Fan modes. +The code in this repository makes use of a number of features in the 1.18.0 +version of ESPHome, including various Fan modes and external components. -### Step 3: Clone this repository into your ESPHome configuration directory +### Step 3: Add this repository as an external component -This repository needs to live in your ESPHome configuration directory, as it -doesn't work correctly when used as a Platform.IO library, and there doesn't -seem to be an analog for that functionality for ESPHome code. +Add this repository to your ESPHome config: -On Hass.IO, you'll want to do something like: +```yaml +external_components: + - source: github://geoffdavis/esphome-mitsubishiheatpump +``` -* Change directories to your esphome configuration directory. -* `mkdir -p src` -* `cd src` -* `git clone https://github.com/geoffdavis/esphome-mitsubishiheatpump.git` +### Step 4: Configure the heatpump -### Step 4: Configure your ESPHome device with YAML - -Create an ESPHome YAML configuration with the following sections: - * `esphome: libraries: [https://github.com/SwiCago/HeatPump]` - * `esphome: includes: [src/esphome-mitsubishiheatpump]` - * `climate:` - set up a custom climate entry, change the Serial port as needed. - * ESP8266 only: `logger: baud_rate: 0` - disable serial port logging on the - sole ESP8266 hardware UART - -The custom climate definition should use `platform: custom` and contain a -`lambda` block, where you instanciate an instance of the MitsubishiHeatPump -class, and then register it with ESPHome. It should allso contain a "climates" -entry. On ESP32 you -can change `&Serial` to `&Serial1` or `&Serial2` and re-enable logging to the -main serial port. - -If that's all greek to you, here's an example. Change "My Heat Pump" to -whatever you want. +Add a `mitsubishi_heatpump` to your ESPHome config: ```yaml climate: - - platform: custom - lambda: |- - auto my_heatpump = new MitsubishiHeatPump(&Serial); - App.register_component(my_heatpump); - return {my_heatpump}; - climates: - - name: "My Heat Pump" + - platform: mitsubishi_heatpump + name: "My Heat Pump" + + # Optional + hardware_uart: UART0 + + # Optional + update_period: 500ms ``` +On ESP8266 you'll need to disable logging to serial because it conflicts with +the heatpump UART: + +```yaml +logger: + baud_rate: 0 +``` + +On ESP32 you can change `hardware_uart` to `UART1` or `UART2` and keep logging +enabled on the main serial port. + Note: this component DOES NOT use the ESPHome `uart` component, as it requires direct access to a hardware UART via the Arduino `HardwareSerial` class. The Mitsubishi Heatpump units use an atypical serial port setting ("even parity"). @@ -119,12 +113,6 @@ esphome: board: esp01_1m # Boards tested: ESP-01S (ESP8266), Wemos D1 Mini (ESP8266); ESP32 Wifi-DevKit2 - libraries: - - https://github.com/SwiCago/HeatPump - - includes: - - src/esphome-mitsubishiheatpump - wifi: ssid: !secret wifi_ssid password: !secret wifi_password @@ -181,18 +169,39 @@ sensor: name: denheatpump WiFi Signal update_interval: 60s +external_components: + - source: github://geoffdavis/esphome-mitsubishiheatpump climate: - - platform: custom - # ESP32 only - change &Serial to &Serial1 or &Serial2 and remove the + - platform: mitsubishi_heatpump + name: "Den Heat Pump" + + # ESP32 only - change UART0 to UART1 or UART2 and remove the # logging:baud_rate above to allow the built-in UART0 to function for # logging. - lambda: |- - auto my_heatpump = new MitsubishiHeatPump(&Serial); - App.register_component(my_heatpump); - return {my_heatpump}; - climates: - - name: "Den Heat Pump" + hardware_uart: UART0 +``` + +# Advanced + +Some models of heat pump require different baud rates or don't support all +possible modes of operation. You can configure pretty much everything in YAML +to match what your hardware supports. For example: + +```yaml +climate: + - platform: mitsubishi_heatpump + name: "My heat pump" + hardware_uart: UART2 + baud_rate: 9600 + supports: + mode: [AUTO, COOL, HEAT, FAN_ONLY] + fan_mode: [AUTO, LOW, MEDIUM, HIGH] + swing_mode: [OFF, VERTICAL] + visual: + min_temperature: 16 + max_temperature: 31 + temperature_step: 1.0 ``` # See Also diff --git a/components/mitsubishi_heatpump/__init__.py b/components/mitsubishi_heatpump/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/mitsubishi_heatpump/climate.py b/components/mitsubishi_heatpump/climate.py new file mode 100644 index 0000000..1f5bfa4 --- /dev/null +++ b/components/mitsubishi_heatpump/climate.py @@ -0,0 +1,88 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate +from esphome.components.logger import HARDWARE_UART_TO_SERIAL +from esphome.const import ( + CONF_ID, + CONF_HARDWARE_UART, + CONF_BAUD_RATE, + CONF_UPDATE_INTERVAL, + CONF_MODE, + CONF_FAN_MODE, + CONF_SWING_MODE, +) +from esphome.core import CORE, coroutine + +AUTO_LOAD = ["climate"] + +CONF_SUPPORTS = "supports" +DEFAULT_CLIMATE_MODES = ['AUTO', 'COOL', 'HEAT', 'DRY', 'FAN_ONLY'] +DEFAULT_FAN_MODES = ['AUTO', 'DIFFUSE', 'LOW', 'MEDIUM', 'MIDDLE', 'HIGH'] +DEFAULT_SWING_MODES = ['OFF', 'VERTICAL'] + +MitsubishiHeatPump = cg.global_ns.class_("MitsubishiHeatPump", climate.Climate, cg.PollingComponent) + + +def valid_uart(uart): + if CORE.is_esp8266: + uarts = ["UART0"] # UART1 is tx-only + elif CORE.is_esp32: + uarts = ["UART0", "UART1", "UART2"] + else: + raise NotImplementedError + + return cv.one_of(*uarts, upper=True)(uart) + + +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, + + # If polling interval is greater than 9 seconds, the HeatPump library + # reconnects, but doesn't then follow up with our data request. + cv.Optional(CONF_UPDATE_INTERVAL, default="500ms"): cv.All( + cv.update_interval, + cv.Range(max=cv.TimePeriod(milliseconds=9000)) + ), + + # Optionally override the supported ClimateTraits. + cv.Optional(CONF_SUPPORTS, default={}): cv.Schema( + { + cv.Optional(CONF_MODE, default=DEFAULT_CLIMATE_MODES): + cv.ensure_list(climate.validate_climate_mode), + cv.Optional(CONF_FAN_MODE, default=DEFAULT_FAN_MODES): + cv.ensure_list(climate.validate_climate_fan_mode), + cv.Optional(CONF_SWING_MODE, default=DEFAULT_SWING_MODES): + cv.ensure_list(climate.validate_climate_swing_mode), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +@coroutine +def to_code(config): + serial = HARDWARE_UART_TO_SERIAL[config[CONF_HARDWARE_UART]] + var = cg.new_Pvariable(config[CONF_ID], cg.RawExpression(f'&{serial}')) + + if CONF_BAUD_RATE in config: + cg.add(var.set_baud_rate(config[CONF_BAUD_RATE])) + + traits = [] + for mode in config[CONF_SUPPORTS][CONF_MODE]: + if mode == 'OFF': + continue + traits.append(f'set_supports_{mode.lower()}_mode') + for mode in config[CONF_SUPPORTS][CONF_FAN_MODE]: + traits.append(f'set_supports_fan_mode_{mode.lower()}') + for mode in config[CONF_SUPPORTS][CONF_SWING_MODE]: + traits.append(f'set_supports_swing_mode_{mode.lower()}') + for trait in traits: + cg.add(getattr(var.config_traits(), trait)(True)) + + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + cg.add_library("https://github.com/SwiCago/HeatPump", None) + diff --git a/espmhp.cpp b/components/mitsubishi_heatpump/espmhp.cpp similarity index 92% rename from espmhp.cpp rename to components/mitsubishi_heatpump/espmhp.cpp index d182fbe..138af06 100644 --- a/espmhp.cpp +++ b/components/mitsubishi_heatpump/espmhp.cpp @@ -32,7 +32,15 @@ MitsubishiHeatPump::MitsubishiHeatPump( ) : PollingComponent{poll_interval}, // member initializers list hw_serial_{hw_serial} -{ } +{ + this->traits_.set_supports_action(true); + this->traits_.set_supports_current_temperature(true); + this->traits_.set_supports_two_point_target_temperature(false); + this->traits_.set_supports_away(false); + 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); +} void MitsubishiHeatPump::check_logger_conflict_() { #ifdef USE_LOGGER @@ -56,8 +64,12 @@ void MitsubishiHeatPump::update() { #endif } +void MitsubishiHeatPump::set_baud_rate(int baud) { + this->baud_ = baud; +} + /** - * Define our supported traits. + * Get our supported traits. * * Note: * Many of the following traits are only available in the 1.5.0 dev train of @@ -67,33 +79,17 @@ void MitsubishiHeatPump::update() { * This class' supported climate::ClimateTraits. */ climate::ClimateTraits MitsubishiHeatPump::traits() { - auto traits = climate::ClimateTraits(); - traits.set_supports_action(true); - traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(true); - traits.set_supports_heat_mode(true); - traits.set_supports_dry_mode(true); - traits.set_supports_fan_only_mode(true); - traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); - traits.set_visual_min_temperature(ESPMHP_MIN_TEMPERATURE); - traits.set_visual_max_temperature(ESPMHP_MAX_TEMPERATURE); - traits.set_visual_temperature_step(ESPMHP_TEMPERATURE_STEP); - traits.set_supports_fan_mode_on(false); - traits.set_supports_fan_mode_off(false); - traits.set_supports_fan_mode_auto(true); - traits.set_supports_fan_mode_focus(false); - traits.set_supports_fan_mode_diffuse(true); - traits.set_supports_fan_mode_low(true); - traits.set_supports_fan_mode_medium(true); - traits.set_supports_fan_mode_middle(true); - traits.set_supports_fan_mode_high(true); - traits.set_supports_swing_mode_off(true); - traits.set_supports_swing_mode_both(false); - traits.set_supports_swing_mode_vertical(true); - traits.set_supports_swing_mode_horizontal(false); - return traits; + return traits_; +} + +/** + * Modify our supported traits. + * + * Returns: + * A reference to this class' supported climate::ClimateTraits. + */ +climate::ClimateTraits& MitsubishiHeatPump::config_traits() { + return traits_; } /** @@ -451,7 +447,7 @@ void MitsubishiHeatPump::setup() { ESP_LOGCONFIG(TAG, "Calling hp->connect(%p)", this->get_hw_serial_()); - if (hp->connect(this->get_hw_serial_())) { + if (hp->connect(this->get_hw_serial_(), this->baud_)) { hp->sync(); } else { diff --git a/espmhp.h b/components/mitsubishi_heatpump/espmhp.h similarity index 91% rename from espmhp.h rename to components/mitsubishi_heatpump/espmhp.h index ee431d3..be2883d 100644 --- a/espmhp.h +++ b/components/mitsubishi_heatpump/espmhp.h @@ -62,6 +62,9 @@ class MitsubishiHeatPump : public PollingComponent, public climate::Climate { ESPMHP_VERSION); } + // Set the baud rate. Must be called before setup() to have any effect. + void set_baud_rate(int); + // print the current configuration void dump_config() override; @@ -80,6 +83,9 @@ class MitsubishiHeatPump : public PollingComponent, public climate::Climate { // Configure the climate object with traits that we support. climate::ClimateTraits traits() override; + // Get a mutable reference to the traits that we support. + climate::ClimateTraits& config_traits(); + // Debugging function to print the object's state. void dump_state(); @@ -90,6 +96,9 @@ class MitsubishiHeatPump : public PollingComponent, public climate::Climate { // HeatPump object using the underlying Arduino library. HeatPump* hp; + // The ClimateTraits supported by this HeatPump. + climate::ClimateTraits traits_; + // Allow the HeatPump class to use get_hw_serial_ friend class HeatPump; @@ -118,7 +127,7 @@ class MitsubishiHeatPump : public PollingComponent, public climate::Climate { private: // Retrieve the HardwareSerial pointer from friend and subclasses. HardwareSerial *hw_serial_; - + int baud_ = 0; }; #endif