├── LICENSE ├── README.md ├── classic ├── README.md ├── home-assistant.io │ └── custom_components │ │ └── mitsubishi_mqtt │ │ ├── __init__.py │ │ ├── climate.py │ │ └── manifest.json └── sketch │ └── mitsubishi_heatpump_mqtt_esp8266_esp32 │ ├── mitsubishi_heatpump_mqtt_esp8266_esp32.h │ └── mitsubishi_heatpump_mqtt_esp8266_esp32.ino ├── libraries ├── HeatPump │ ├── HeatPump.cpp │ └── HeatPump.h └── README.md ├── mitsucon.h └── mitsucon.ino /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MitsuCon 2 | Mitsubishi Heat Pump (Air Conditioner) Controller for Home Assistant 3 | 4 | *New version with hvac_action. Plase increase PubSubClient.h MQTT_MAX_PACKET_SIZE to 2048. 5 | 6 | ![Wemos D1 Mini Adapter](https://user-images.githubusercontent.com/44964969/51798270-c3392980-2242-11e9-8986-cffc5fe4d287.jpg) 7 | 8 | Build ESP-8266 device to interface between Mitsubishi Electric heat pump and Home Assistant. This code utilizes Home Assistant native MQTT HVAC component with MQTT discovery feature so it should be compatible with future Home Assistant updates and will be added to Home Assistant automatically without any configuration. 9 | 10 | ## Hardware 11 | The easiest way to buid a controller is to use Wemos D1 mini board since it already has 5V pin. Only board V2 is working out of box, not current board V3. Notice the big shield on board when you place order. 12 | 13 | ![wemos](https://user-images.githubusercontent.com/44964969/76160597-137cfd80-615e-11ea-8bf2-d9a6be363fc5.jpg) 14 | 15 | A connector to CN105 port on heat pump is JST PAP-05V-S. 16 | 17 | ## Build a code 18 | Using Arduino IDE to modify configuration and complie file for your hardware. 19 | 20 | Arduino IDE 1.8.10 21 | ArduinoJson 6.12.0 22 | PubSubClient 2.7.0 23 | 24 | Edit file Arduino\libraries\PubSubClient\PubSubClient.h MQTT_MAX_PACKET_SIZE to 2048. 25 | 26 | Tested on Wemos D1 mini board with Home Assistant 0.96 and Mosquitto broker add-on 5.0. 27 | 28 | ## Known Issues 29 | * Sometimes heat pump unit turns off instead when send temperature changing command. 30 | * When change setting on HA UI, change will effect immediately but UI will delay for next data pull to refresh new status. 31 | * Cannot use imperial (fahrenheit) unit. Waiting for Home Assistant support soon. 32 | 33 | ## Classic Code 34 | The Classic folder is a code proven to working well on Home Assistant < 0.100.0. It has 2 parts: 35 | 36 | * arduino sketch for ESP8266/ESP32 board 37 | * Home Assistant custom component file to place in Home Assistant server 38 | 39 | Chris Davis has a very detailed instruction blog: 40 | 41 | https://chrdavis.github.io/hacking-a-mitsubishi-heat-pump-Part-1/ 42 | 43 | https://chrdavis.github.io/hacking-a-mitsubishi-heat-pump-Part-2/ 44 | 45 | ## Other Projects 46 | Interesting projects developed with different design choices: 47 | 48 | WiFi manager and web interface 49 | https://github.com/gysmo38/mitsubishi2MQTT 50 | 51 | Enhanced WiFi manager, web interface and mroe functions 52 | https://github.com/dzungpv/mitsubishi2MQTT 53 | 54 | Added Platform IO, Added SSL, Added an RGB LED 55 | https://github.com/SittingDuc/MitsuCon 56 | 57 | 58 | ## Credits 59 | Thanks to all contributors especially: 60 | * lekobob https://github.com/lekobob/mitsu_mqtt 61 | * SwiCago https://github.com/SwiCago/HeatPump 62 | * Hadley https://nicegear.co.nz/blog/hacking-a-mitsubishi-heat-pump-air-conditioner/ 63 | 64 | *Mitsubishi means Mitsubishi Electric, not Mitsubishi Heavy Industries. 65 | -------------------------------------------------------------------------------- /classic/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Classic Controller 3 | 4 | TESTED with HA <=0.99.3 - added heat/cool mode and hvac_action. 5 | 6 | This code is proven to working well on Home Assistant for years before HA developed native MQTT HVAC template. Since it creates a custom component directly in HA it works faster than native component. User interface is responsed almost instantly. However, a custom component aldo need to be maintained along each release of Home Assistant for frequently break changes. 7 | 8 | Classic Controller has 2 parts: 9 | 10 | * arduino sketch for ESP8266/ESP32 board 11 | * Home Assistant custom component file to place in Home Assistant server 12 | 13 | Chris Davis has a very detailed instruction blog: 14 | 15 | https://chrdavis.github.io/hacking-a-mitsubishi-heat-pump-Part-1/ 16 | 17 | https://chrdavis.github.io/hacking-a-mitsubishi-heat-pump-Part-2/ 18 | 19 | ## Arduino 20 | This sketch supports ESP8266 and ESP32 board. Modify your credentials like WiFi password and MQTT server in mitsubishi_heatpump_mqtt_esp8266_esp32.h first and then compile and flash mitsubishi_heatpump_mqtt_esp8266_esp32.ino to baord. 21 | 22 | ## Home Assistant 23 | You need MQTT Server like Mosquito broker add-ons. 24 | 25 | ### Installation 26 | Copy custom_components folder to home assistant config directory. 27 | Resulting folder structure should be /custom_components/mitsubishi_mqtt/climate.py 28 | 29 | Add the following to your configuration.yaml: 30 | ```c++ 31 | climate: 32 | - platform: mitsubishi_mqtt 33 | name: "Mistubishi Heatpump" 34 | command_topic: "heatpump/set" 35 | temperature_state_topic: "heatpump/status" 36 | state_topic: "heatpump" 37 | 38 | ``` 39 | if your device is not support HEAT mode, like air conditioner unit, define `modes` for supported modes. 40 | ```c++ 41 | climate: 42 | - platform: mitsubishi_mqtt 43 | name: "Mistubishi Heatpump" 44 | modes: 45 | - "AUTO" 46 | - "COOL" 47 | - "DRY" 48 | - "FAN" 49 | - "OFF" 50 | command_topic: "heatpump/set" 51 | temperature_state_topic: "heatpump/status" 52 | state_topic: "heatpump" 53 | 54 | ``` 55 | -------------------------------------------------------------------------------- /classic/home-assistant.io/custom_components/mitsubishi_mqtt/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /classic/home-assistant.io/custom_components/mitsubishi_mqtt/climate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for Mitsubishi heatpumps using https://github.com/SwiCago/HeatPump over MQTT. 3 | 4 | For more details about this platform, please refer to the documentation at 5 | https://github.com/lekobob/mitsu_mqtt 6 | """ 7 | 8 | import logging 9 | 10 | import voluptuous as vol 11 | 12 | from homeassistant.components.mqtt import ( 13 | CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, MqttAttributes, MqttAvailability, 14 | subscription) 15 | 16 | from homeassistant.components.mqtt.climate import ( 17 | CONF_TEMP_STATE_TOPIC, CONF_MODE_LIST) 18 | from homeassistant.components.climate import ( 19 | ClimateDevice) 20 | from homeassistant.components.climate.const import ( 21 | SUPPORT_TARGET_TEMPERATURE, 22 | SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, 23 | HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_HEAT, HVAC_MODE_FAN_ONLY, 24 | CURRENT_HVAC_OFF, CURRENT_HVAC_HEAT, CURRENT_HVAC_COOL, CURRENT_HVAC_DRY, CURRENT_HVAC_IDLE, CURRENT_HVAC_FAN) 25 | from homeassistant.const import ( 26 | CONF_NAME, CONF_VALUE_TEMPLATE, TEMP_CELSIUS, ATTR_TEMPERATURE) 27 | 28 | import homeassistant.components.mqtt as mqtt 29 | import homeassistant.helpers.config_validation as cv 30 | from homeassistant.util.temperature import convert as convert_temp 31 | from numbers import Number 32 | import json 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | DEPENDENCIES = ['mqtt'] 37 | 38 | DEFAULT_NAME = 'MQTT Climate' 39 | 40 | SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE 41 | 42 | AVAILABLE_MODES = ["AUTO", "COOL", "DRY", "HEAT", "FAN", "OFF"] 43 | 44 | PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ 45 | vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, 46 | vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, 47 | vol.Optional(CONF_MODE_LIST, default=AVAILABLE_MODES): cv.ensure_list, 48 | }) 49 | 50 | TARGET_TEMPERATURE_STEP = 1 51 | 52 | ha_to_me = {HVAC_MODE_HEAT_COOL: 'AUTO', HVAC_MODE_COOL: 'COOL', HVAC_MODE_DRY: 'DRY', HVAC_MODE_HEAT: 'HEAT', HVAC_MODE_FAN_ONLY: 'FAN', HVAC_MODE_OFF: 'OFF'} 53 | me_to_ha = {v: k for k, v in ha_to_me.items()} 54 | 55 | # pylint: disable=unused-argument 56 | async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): 57 | """Setup the MQTT climate device.""" 58 | value_template = config.get(CONF_VALUE_TEMPLATE) 59 | if value_template is not None: 60 | value_template.hass = hass 61 | async_add_devices([MqttClimate( 62 | hass, 63 | config.get(CONF_NAME), 64 | config.get(CONF_STATE_TOPIC), 65 | config.get(CONF_TEMP_STATE_TOPIC), 66 | config.get(CONF_COMMAND_TOPIC), 67 | config.get(CONF_MODE_LIST), 68 | config.get(CONF_QOS), 69 | config.get(CONF_RETAIN), 70 | )]) 71 | 72 | 73 | class MqttClimate(ClimateDevice): 74 | """Representation of a Mistsubishi Minisplit Heatpump controlled over MQTT.""" 75 | 76 | 77 | def __init__(self, hass, name, state_topic, temperature_state_topic, command_topic, modes, qos, retain): 78 | """Initialize the MQTT Heatpump.""" 79 | self._state = False 80 | self._hass = hass 81 | self.hass = hass 82 | self._name = name 83 | self._state_topic = state_topic 84 | self._temperature_state_topic = temperature_state_topic 85 | self._command_topic = command_topic 86 | self._qos = qos 87 | self._retain = retain 88 | self._current_temperature = None 89 | self._target_temperature = None 90 | self._fan_modes = ["AUTO", "QUIET", "1", "2", "3", "4"] 91 | self._fan_mode = None 92 | self._hvac_modes = modes 93 | self._hvac_mode = None 94 | self._current_power = None 95 | self._current_status = False 96 | self._swing_modes = ["AUTO", "1", "2", "3", "4", "5", "SWING"] 97 | self._swing_mode = None 98 | self._sub_state = None 99 | 100 | async def async_added_to_hass(self): 101 | """Handle being added to home assistant.""" 102 | await super().async_added_to_hass() 103 | await self._subscribe_topics() 104 | 105 | async def _subscribe_topics(self): 106 | """(Re)Subscribe to topics.""" 107 | topics = {} 108 | 109 | def add_subscription(topics, topic, msg_callback): 110 | if topic is not None: 111 | topics[topic] = { 112 | 'topic': topic, 113 | 'msg_callback': msg_callback, 114 | 'qos': self._qos} 115 | 116 | def message_received(msg): 117 | """A new MQTT message has been received.""" 118 | topic = msg.topic 119 | payload = msg.payload 120 | parsed = json.loads(payload) 121 | if topic == self._state_topic: 122 | self._target_temperature = float(parsed['temperature']) 123 | self._fan_mode = parsed['fan'] 124 | self._swing_mode = parsed['vane'] 125 | if parsed['power'] == "OFF": 126 | _LOGGER.debug("Power Off") 127 | self._hvac_mode = "OFF" 128 | self._current_power = "OFF" 129 | else: 130 | _LOGGER.debug("Power On") 131 | self._hvac_mode = parsed['mode'] 132 | self._current_power = "ON" 133 | elif topic == self._temperature_state_topic: 134 | _LOGGER.debug('Room Temp: {0}'.format(parsed['roomTemperature'])) 135 | self._current_temperature = float(parsed['roomTemperature']) 136 | self._current_status = bool(parsed['operating']) 137 | else: 138 | print("unknown topic") 139 | self.async_write_ha_state() 140 | _LOGGER.debug("Power=%s, Operation=%s", self._current_power, self._hvac_mode) 141 | 142 | for topic in [self._state_topic, self._temperature_state_topic]: 143 | add_subscription(topics, topic, message_received) 144 | 145 | self._sub_state = await subscription.async_subscribe_topics( 146 | self.hass, self._sub_state, topics) 147 | 148 | 149 | async def async_will_remove_from_hass(self): 150 | """Unsubscribe when removed.""" 151 | self._sub_state = await subscription.async_unsubscribe_topics( 152 | self.hass, self._sub_state) 153 | await MqttAttributes.async_will_remove_from_hass(self) 154 | await MqttAvailability.async_will_remove_from_hass(self) 155 | 156 | @property 157 | def supported_features(self): 158 | """Return the list of supported features.""" 159 | return SUPPORT_FLAGS 160 | 161 | @property 162 | def target_temperature_step(self): 163 | """Return the target temperature step.""" 164 | return TARGET_TEMPERATURE_STEP 165 | 166 | @property 167 | def should_poll(self): 168 | """Polling not needed for a demo climate device.""" 169 | return False 170 | 171 | @property 172 | def name(self): 173 | """Return the name of the climate device.""" 174 | return self._name 175 | 176 | @property 177 | def temperature_unit(self): 178 | """Return the unit of measurement.""" 179 | return TEMP_CELSIUS 180 | 181 | @property 182 | def target_temperature(self): 183 | """Return the temperature we try to reach.""" 184 | return self._target_temperature 185 | 186 | @property 187 | def current_temperature(self): 188 | """Return the current temperature.""" 189 | return self._current_temperature 190 | 191 | @property 192 | def fan_mode(self): 193 | """Return the fan setting.""" 194 | if self._fan_mode is None: 195 | return 196 | return self._fan_mode.capitalize() 197 | 198 | @property 199 | def fan_modes(self): 200 | """List of available fan modes.""" 201 | return [k.capitalize() for k in self._fan_modes] 202 | 203 | @property 204 | def hvac_action(self): 205 | if self._current_power == 'OFF': 206 | return CURRENT_HVAC_OFF 207 | if self._hvac_mode == 'FAN': 208 | return CURRENT_HVAC_FAN 209 | if not self._current_status: 210 | return CURRENT_HVAC_IDLE 211 | if self._hvac_mode == 'COOL': 212 | return CURRENT_HVAC_COOL 213 | if self._hvac_mode == 'HEAT': 214 | return CURRENT_HVAC_HEAT 215 | if self._hvac_mode == 'DRY': 216 | return CURRENT_HVAC_DRY 217 | if self._hvac_mode == 'AUTO' and self._current_temperature > self._target_temperature: 218 | return CURRENT_HVAC_COOL 219 | if self._hvac_mode == 'AUTO' and self._current_temperature <= self._target_temperature: 220 | return CURRENT_HVAC_HEAT 221 | 222 | @property 223 | def hvac_mode(self): 224 | """Return current operation ie. heat, cool, idle.""" 225 | if self._hvac_mode is None: 226 | return HVAC_MODE_OFF 227 | if self._current_power == "OFF": 228 | return HVAC_MODE_OFF 229 | else: 230 | return me_to_ha[self._hvac_mode] 231 | 232 | @property 233 | def hvac_modes(self): 234 | """List of available operation modes.""" 235 | return [me_to_ha[k] for k in self._hvac_modes] 236 | 237 | @property 238 | def swing_mode(self): 239 | """Return the swing setting.""" 240 | if self._swing_mode is None: 241 | return 242 | return self._swing_mode.capitalize() 243 | 244 | @property 245 | def swing_modes(self): 246 | """List of available swing modes.""" 247 | return [k.capitalize() for k in self._swing_modes] 248 | 249 | async def async_set_temperature(self, **kwargs): 250 | """Set new target temperatures.""" 251 | if kwargs.get(ATTR_TEMPERATURE) is not None: 252 | # This is also be set via the mqtt callback 253 | self._target_temperature = kwargs.get(ATTR_TEMPERATURE) 254 | self._publish_temperature() 255 | self.async_write_ha_state() 256 | 257 | async def async_set_fan_mode(self, fan_mode): 258 | """Set new fan mode.""" 259 | if fan_mode is not None: 260 | self._fan_mode = fan_mode.upper() 261 | payload = '{"fan":"' + self._fan_mode + '"}' 262 | mqtt.async_publish(self.hass, self._command_topic, payload, 263 | self._qos, self._retain) 264 | self.async_write_ha_state() 265 | 266 | async def async_set_hvac_mode(self, hvac_mode): 267 | """Set new operating mode.""" 268 | if hvac_mode is not None: 269 | self._hvac_mode = ha_to_me[hvac_mode] 270 | if self._hvac_mode == "OFF": 271 | payload = '{"power":"OFF"}' 272 | self._current_power = "OFF" 273 | else: 274 | payload = '{"power":"ON","mode":"' + self._hvac_mode + '"}' 275 | self._current_power = "ON" 276 | mqtt.async_publish(self.hass, self._command_topic, payload, 277 | self._qos, self._retain) 278 | self.async_write_ha_state() 279 | 280 | async def async_set_swing_mode(self, swing_mode): 281 | """Set new swing mode.""" 282 | if swing_mode is not None: 283 | self._swing_mode = swing_mode.upper() 284 | payload = '{"vane":"' + self._swing_mode + '"}' 285 | mqtt.async_publish(self.hass, self._command_topic, payload, 286 | self._qos, self._retain) 287 | self.async_write_ha_state() 288 | 289 | def _publish_temperature(self): 290 | if self._target_temperature is None: 291 | return 292 | unencoded = '{"temperature":' + str(round(self._target_temperature * 2) / 2.0) + '}' 293 | mqtt.async_publish(self.hass, self._command_topic, unencoded, 294 | self._qos, self._retain) 295 | -------------------------------------------------------------------------------- /classic/home-assistant.io/custom_components/mitsubishi_mqtt/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mitsubishi_mqtt", 3 | "name": "MitsubishiMqtt", 4 | "documentation": "https://github.com/SwiCago/HeatPump/tree/master/integrations/home-assistant.io/custom_components/mitsubishi_mqtt", 5 | "dependencies": [], 6 | "codeowners": ["@SwiCago","@lekobob","@dzungpv"], 7 | "requirements": [] 8 | } 9 | -------------------------------------------------------------------------------- /classic/sketch/mitsubishi_heatpump_mqtt_esp8266_esp32/mitsubishi_heatpump_mqtt_esp8266_esp32.h: -------------------------------------------------------------------------------- 1 | 2 | //#define ESP32 3 | //#define OTA 4 | //const char* ota_password = ""; 5 | 6 | // wifi settings 7 | const char* ssid = ""; 8 | const char* password = ""; 9 | 10 | // mqtt server settings 11 | const char* mqtt_server = ""; 12 | const int mqtt_port = 1883; 13 | const char* mqtt_username = ""; 14 | const char* mqtt_password = ""; 15 | 16 | // mqtt client settings 17 | // Note PubSubClient.h has a MQTT_MAX_PACKET_SIZE of 128 defined, so either raise it to 256 or use short topics 18 | const char* client_id = "heatpump"; // Must be unique on the MQTT network 19 | const char* heatpump_topic = "heatpump"; 20 | const char* heatpump_set_topic = "heatpump/set"; 21 | const char* heatpump_status_topic = "heatpump/status"; 22 | const char* heatpump_timers_topic = "heatpump/timers"; 23 | 24 | const char* heatpump_debug_topic = "heatpump/debug"; 25 | const char* heatpump_debug_set_topic = "heatpump/debug/set"; 26 | 27 | // pinouts 28 | const int redLedPin = 0; // Onboard LED = digital pin 0 (red LED on adafruit ESP8266 huzzah) 29 | const int blueLedPin = 2; // Onboard LED = digital pin 0 (blue LED on adafruit ESP8266 huzzah) 30 | 31 | // sketch settings 32 | const unsigned int SEND_ROOM_TEMP_INTERVAL_MS = 60000; 33 | -------------------------------------------------------------------------------- /classic/sketch/mitsubishi_heatpump_mqtt_esp8266_esp32/mitsubishi_heatpump_mqtt_esp8266_esp32.ino: -------------------------------------------------------------------------------- 1 | 2 | #ifdef ESP32 3 | #include 4 | #else 5 | #include 6 | #endif 7 | #include 8 | #include 9 | #include 10 | 11 | #include "mitsubishi_heatpump_mqtt_esp8266_esp32.h" 12 | 13 | #ifdef OTA 14 | #ifdef ESP32 15 | #include 16 | #include 17 | #else 18 | #include 19 | #endif 20 | #include 21 | #endif 22 | 23 | // wifi, mqtt and heatpump client instances 24 | WiFiClient espClient; 25 | PubSubClient mqtt_client(espClient); 26 | HeatPump hp; 27 | unsigned long lastTempSend; 28 | 29 | // debug mode, when true, will send all packets received from the heatpump to topic heatpump_debug_topic 30 | // this can also be set by sending "on" to heatpump_debug_set_topic 31 | bool _debugMode = true; 32 | 33 | 34 | void setup() { 35 | pinMode(redLedPin, OUTPUT); 36 | digitalWrite(redLedPin, HIGH); 37 | pinMode(blueLedPin, OUTPUT); 38 | digitalWrite(blueLedPin, HIGH); 39 | 40 | WiFi.hostname(client_id); 41 | WiFi.mode(WIFI_STA); 42 | WiFi.begin(ssid, password); 43 | 44 | while (WiFi.status() != WL_CONNECTED) { 45 | // wait 500ms, flashing the blue LED to indicate WiFi connecting... 46 | digitalWrite(blueLedPin, LOW); 47 | delay(250); 48 | digitalWrite(blueLedPin, HIGH); 49 | delay(250); 50 | } 51 | 52 | // startup mqtt connection 53 | mqtt_client.setServer(mqtt_server, mqtt_port); 54 | mqtt_client.setCallback(mqttCallback); 55 | mqttConnect(); 56 | 57 | // connect to the heatpump. Callbacks first so that the hpPacketDebug callback is available for connect() 58 | hp.setSettingsChangedCallback(hpSettingsChanged); 59 | hp.setStatusChangedCallback(hpStatusChanged); 60 | hp.setPacketCallback(hpPacketDebug); 61 | 62 | #ifdef OTA 63 | ArduinoOTA.setHostname(client_id); 64 | ArduinoOTA.setPassword(ota_password); 65 | ArduinoOTA.begin(); 66 | #endif 67 | 68 | hp.connect(&Serial); 69 | 70 | lastTempSend = millis(); 71 | } 72 | 73 | void hpSettingsChanged() { 74 | const size_t bufferSize = JSON_OBJECT_SIZE(6); 75 | DynamicJsonDocument root(bufferSize); 76 | 77 | heatpumpSettings currentSettings = hp.getSettings(); 78 | 79 | root["power"] = currentSettings.power; 80 | root["mode"] = currentSettings.mode; 81 | root["temperature"] = currentSettings.temperature; 82 | root["fan"] = currentSettings.fan; 83 | root["vane"] = currentSettings.vane; 84 | root["wideVane"] = currentSettings.wideVane; 85 | //root["iSee"] = currentSettings.iSee; 86 | 87 | char buffer[512]; 88 | serializeJson(root, buffer); 89 | 90 | bool retain = true; 91 | if (!mqtt_client.publish(heatpump_topic, buffer, retain)) { 92 | mqtt_client.publish(heatpump_debug_topic, "failed to publish to heatpump topic"); 93 | } 94 | } 95 | 96 | void hpStatusChanged(heatpumpStatus currentStatus) { 97 | // send room temp and operating info 98 | const size_t bufferSizeInfo = JSON_OBJECT_SIZE(2); 99 | DynamicJsonDocument rootInfo(bufferSizeInfo); 100 | 101 | rootInfo["roomTemperature"] = currentStatus.roomTemperature; 102 | rootInfo["operating"] = currentStatus.operating; 103 | 104 | char bufferInfo[512]; 105 | serializeJson(rootInfo, bufferInfo); 106 | 107 | if (!mqtt_client.publish(heatpump_status_topic, bufferInfo, true)) { 108 | mqtt_client.publish(heatpump_debug_topic, "failed to publish to room temp and operation status to heatpump/status topic"); 109 | } 110 | 111 | // send the timer info 112 | const size_t bufferSizeTimers = JSON_OBJECT_SIZE(5); 113 | DynamicJsonDocument rootTimers(bufferSizeTimers); 114 | 115 | rootTimers["mode"] = currentStatus.timers.mode; 116 | rootTimers["onMins"] = currentStatus.timers.onMinutesSet; 117 | rootTimers["onRemainMins"] = currentStatus.timers.onMinutesRemaining; 118 | rootTimers["offMins"] = currentStatus.timers.offMinutesSet; 119 | rootTimers["offRemainMins"] = currentStatus.timers.offMinutesRemaining; 120 | 121 | char bufferTimers[512]; 122 | serializeJson(rootTimers, bufferTimers); 123 | 124 | if (!mqtt_client.publish(heatpump_timers_topic, bufferTimers, true)) { 125 | mqtt_client.publish(heatpump_debug_topic, "failed to publish timer info to heatpump/status topic"); 126 | } 127 | } 128 | 129 | void hpPacketDebug(byte* packet, unsigned int length, char* packetDirection) { 130 | if (_debugMode) { 131 | String message; 132 | for (int idx = 0; idx < length; idx++) { 133 | if (packet[idx] < 16) { 134 | message += "0"; // pad single hex digits with a 0 135 | } 136 | message += String(packet[idx], HEX) + " "; 137 | } 138 | 139 | const size_t bufferSize = JSON_OBJECT_SIZE(6); 140 | DynamicJsonDocument root(bufferSize); 141 | 142 | root[packetDirection] = message; 143 | 144 | char buffer[512]; 145 | serializeJson(root, buffer); 146 | 147 | if (!mqtt_client.publish(heatpump_debug_topic, buffer)) { 148 | mqtt_client.publish(heatpump_debug_topic, "failed to publish to heatpump/debug topic"); 149 | } 150 | } 151 | } 152 | 153 | void mqttCallback(char* topic, byte* payload, unsigned int length) { 154 | // Copy payload into message buffer 155 | char message[length + 1]; 156 | for (int i = 0; i < length; i++) { 157 | message[i] = (char)payload[i]; 158 | } 159 | message[length] = '\0'; 160 | 161 | if (strcmp(topic, heatpump_set_topic) == 0) { //if the incoming message is on the heatpump_set_topic topic... 162 | // Parse message into JSON 163 | const size_t bufferSize = JSON_OBJECT_SIZE(6); 164 | DynamicJsonDocument root(bufferSize); 165 | DeserializationError error = deserializeJson(root, message); 166 | 167 | if (error) { 168 | mqtt_client.publish(heatpump_debug_topic, "!root.success(): invalid JSON on heatpump_set_topic..."); 169 | return; 170 | } 171 | 172 | // Step 3: Retrieve the values 173 | if (root.containsKey("power")) { 174 | const char* power = root["power"]; 175 | hp.setPowerSetting(power); 176 | } 177 | 178 | if (root.containsKey("mode")) { 179 | const char* mode = root["mode"]; 180 | hp.setModeSetting(mode); 181 | } 182 | 183 | if (root.containsKey("temperature")) { 184 | float temperature = root["temperature"]; 185 | hp.setTemperature(temperature); 186 | } 187 | 188 | if (root.containsKey("fan")) { 189 | const char* fan = root["fan"]; 190 | hp.setFanSpeed(fan); 191 | } 192 | 193 | if (root.containsKey("vane")) { 194 | const char* vane = root["vane"]; 195 | hp.setVaneSetting(vane); 196 | } 197 | 198 | if (root.containsKey("wideVane")) { 199 | const char* wideVane = root["wideVane"]; 200 | hp.setWideVaneSetting(wideVane); 201 | } 202 | 203 | if (root.containsKey("remoteTemp")) { 204 | float remoteTemp = root["remoteTemp"]; 205 | hp.setRemoteTemperature(remoteTemp); 206 | } 207 | else if (root.containsKey("custom")) { 208 | String custom = root["custom"]; 209 | 210 | // copy custom packet to char array 211 | char buffer[(custom.length() + 1)]; // +1 for the NULL at the end 212 | custom.toCharArray(buffer, (custom.length() + 1)); 213 | 214 | byte bytes[20]; // max custom packet bytes is 20 215 | int byteCount = 0; 216 | char *nextByte; 217 | 218 | // loop over the byte string, breaking it up by spaces (or at the end of the line - \n) 219 | nextByte = strtok(buffer, " "); 220 | while (nextByte != NULL && byteCount < 20) { 221 | bytes[byteCount] = strtol(nextByte, NULL, 16); // convert from hex string 222 | nextByte = strtok(NULL, " "); 223 | byteCount++; 224 | } 225 | 226 | // dump the packet so we can see what it is. handy because you can run the code without connecting the ESP to the heatpump, and test sending custom packets 227 | hpPacketDebug(bytes, byteCount, "customPacket"); 228 | 229 | hp.sendCustomPacket(bytes, byteCount); 230 | } 231 | else { 232 | bool result = hp.update(); 233 | 234 | if (!result) { 235 | mqtt_client.publish(heatpump_debug_topic, "heatpump: update() failed"); 236 | } 237 | } 238 | 239 | } else if (strcmp(topic, heatpump_debug_set_topic) == 0) { //if the incoming message is on the heatpump_debug_set_topic topic... 240 | if (strcmp(message, "on") == 0) { 241 | _debugMode = true; 242 | mqtt_client.publish(heatpump_debug_topic, "debug mode enabled"); 243 | } else if (strcmp(message, "off") == 0) { 244 | _debugMode = false; 245 | mqtt_client.publish(heatpump_debug_topic, "debug mode disabled"); 246 | } 247 | } else {//should never get called, as that would mean something went wrong with subscribe 248 | mqtt_client.publish(heatpump_debug_topic, "heatpump: wrong topic received"); 249 | } 250 | } 251 | 252 | void mqttConnect() { 253 | // Loop until we're reconnected 254 | while (!mqtt_client.connected()) { 255 | // Attempt to connect 256 | if (mqtt_client.connect(client_id, mqtt_username, mqtt_password)) { 257 | mqtt_client.subscribe(heatpump_set_topic); 258 | mqtt_client.subscribe(heatpump_debug_set_topic); 259 | } else { 260 | // Wait 5 seconds before retrying 261 | delay(5000); 262 | } 263 | } 264 | } 265 | 266 | void loop() { 267 | if (!mqtt_client.connected()) { 268 | mqttConnect(); 269 | } 270 | 271 | hp.sync(); 272 | 273 | if (millis() > (lastTempSend + SEND_ROOM_TEMP_INTERVAL_MS)) { // only send the temperature every 60s 274 | hpStatusChanged(hp.getStatus()); 275 | lastTempSend = millis(); 276 | } 277 | 278 | mqtt_client.loop(); 279 | 280 | #ifdef OTA 281 | ArduinoOTA.handle(); 282 | #endif 283 | } 284 | -------------------------------------------------------------------------------- /libraries/HeatPump/HeatPump.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | HeatPump.cpp - Mitsubishi Heat Pump control library for Arduino 3 | Copyright (c) 2017 Al Betschart. All right reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | */ 19 | #include "HeatPump.h" 20 | 21 | // Structures ////////////////////////////////////////////////////////////////// 22 | 23 | bool operator==(const heatpumpSettings& lhs, const heatpumpSettings& rhs) { 24 | return lhs.power == rhs.power && 25 | lhs.mode == rhs.mode && 26 | lhs.temperature == rhs.temperature && 27 | lhs.fan == rhs.fan && 28 | lhs.vane == rhs.vane && 29 | lhs.wideVane == rhs.wideVane && 30 | lhs.iSee == rhs.iSee; 31 | } 32 | 33 | bool operator!=(const heatpumpSettings& lhs, const heatpumpSettings& rhs) { 34 | return lhs.power != rhs.power || 35 | lhs.mode != rhs.mode || 36 | lhs.temperature != rhs.temperature || 37 | lhs.fan != rhs.fan || 38 | lhs.vane != rhs.vane || 39 | lhs.wideVane != rhs.wideVane || 40 | lhs.iSee != rhs.iSee; 41 | } 42 | 43 | bool operator!(const heatpumpSettings& settings) { 44 | return !settings.power && 45 | !settings.mode && 46 | !settings.temperature && 47 | !settings.fan && 48 | !settings.vane && 49 | !settings.wideVane && 50 | !settings.iSee; 51 | } 52 | 53 | bool operator==(const heatpumpTimers& lhs, const heatpumpTimers& rhs) { 54 | return lhs.mode == rhs.mode && 55 | lhs.onMinutesSet == rhs.onMinutesSet && 56 | lhs.onMinutesRemaining == rhs.onMinutesRemaining && 57 | lhs.offMinutesSet == rhs.offMinutesSet && 58 | lhs.offMinutesRemaining == rhs.offMinutesRemaining; 59 | } 60 | 61 | bool operator!=(const heatpumpTimers& lhs, const heatpumpTimers& rhs) { 62 | return lhs.mode != rhs.mode || 63 | lhs.onMinutesSet != rhs.onMinutesSet || 64 | lhs.onMinutesRemaining != rhs.onMinutesRemaining || 65 | lhs.offMinutesSet != rhs.offMinutesSet || 66 | lhs.offMinutesRemaining != rhs.offMinutesRemaining; 67 | } 68 | 69 | 70 | // Constructor ///////////////////////////////////////////////////////////////// 71 | 72 | HeatPump::HeatPump() { 73 | lastSend = 0; 74 | infoMode = 0; 75 | lastRecv = millis() - (PACKET_SENT_INTERVAL_MS * 10); 76 | autoUpdate = false; 77 | firstRun = true; 78 | tempMode = false; 79 | waitForRead = false; 80 | externalUpdate = false; 81 | wideVaneAdj = false; 82 | currentStatus = {0, false, {TIMER_MODE_MAP[0], 0, 0, 0, 0}}; // initialise to all off, then it will update shortly after connect 83 | } 84 | 85 | // Public Methods ////////////////////////////////////////////////////////////// 86 | 87 | bool HeatPump::connect(HardwareSerial *serial) { 88 | return connect(serial,true); 89 | } 90 | 91 | bool HeatPump::connect(HardwareSerial *serial, bool retry) { 92 | if(serial != NULL) { 93 | _HardSerial = serial; 94 | } 95 | connected = false; 96 | _HardSerial->begin(bitrate, SERIAL_8E1); 97 | if(onConnectCallback) { 98 | onConnectCallback(); 99 | } 100 | 101 | // settle before we start sending packets 102 | delay(2000); 103 | 104 | // send the CONNECT packet twice - need to copy the CONNECT packet locally 105 | byte packet[CONNECT_LEN]; 106 | memcpy(packet, CONNECT, CONNECT_LEN); 107 | //for(int count = 0; count < 2; count++) { 108 | writePacket(packet, CONNECT_LEN); 109 | while(!canRead()) { delay(10); } 110 | int packetType = readPacket(); 111 | if(packetType != RCVD_PKT_CONNECT_SUCCESS && retry){ 112 | bitrate = (bitrate == 2400 ? 9600 : 2400); 113 | return connect(serial, false); 114 | } 115 | return packetType == RCVD_PKT_CONNECT_SUCCESS; 116 | //} 117 | } 118 | 119 | bool HeatPump::update() { 120 | while(!canSend(false)) { delay(10); } 121 | 122 | byte packet[PACKET_LEN] = {}; 123 | createPacket(packet, wantedSettings); 124 | writePacket(packet, PACKET_LEN); 125 | 126 | while(!canRead()) { delay(10); } 127 | int packetType = readPacket(); 128 | 129 | if(packetType == RCVD_PKT_UPDATE_SUCCESS) { 130 | // call sync() to get the latest settings from the heatpump for autoUpdate, which should now have the updated settings 131 | if(autoUpdate) { //this sync will happen regardless, but autoUpdate needs it sooner than later. 132 | while(!canSend(true)) { 133 | delay(10); 134 | } 135 | sync(RQST_PKT_SETTINGS); 136 | } 137 | 138 | return true; 139 | } else { 140 | return false; 141 | } 142 | } 143 | 144 | void HeatPump::sync(byte packetType) { 145 | if((!connected) || (millis() - lastRecv > (PACKET_SENT_INTERVAL_MS * 10))) { 146 | connect(NULL); 147 | } 148 | else if(canRead()) { 149 | readPacket(); 150 | } 151 | else if(autoUpdate && !firstRun && wantedSettings != currentSettings && packetType == PACKET_TYPE_DEFAULT) { 152 | update(); 153 | } 154 | else if(canSend(true)) { 155 | byte packet[PACKET_LEN] = {}; 156 | createInfoPacket(packet, packetType); 157 | writePacket(packet, PACKET_LEN); 158 | } 159 | } 160 | 161 | void HeatPump::enableExternalUpdate() { 162 | externalUpdate = true; 163 | } 164 | 165 | void HeatPump::enableAutoUpdate() { 166 | autoUpdate = true; 167 | } 168 | 169 | void HeatPump::disableAutoUpdate() { 170 | autoUpdate = false; 171 | } 172 | 173 | heatpumpSettings HeatPump::getSettings() { 174 | return currentSettings; 175 | } 176 | 177 | bool HeatPump::isConnected() { 178 | return connected; 179 | } 180 | 181 | void HeatPump::setSettings(heatpumpSettings settings) { 182 | setPowerSetting(settings.power); 183 | setModeSetting(settings.mode); 184 | setTemperature(settings.temperature); 185 | setFanSpeed(settings.fan); 186 | setVaneSetting(settings.vane); 187 | setWideVaneSetting(settings.wideVane); 188 | } 189 | 190 | bool HeatPump::getPowerSettingBool() { 191 | return currentSettings.power == POWER_MAP[1] ? true : false; 192 | } 193 | 194 | void HeatPump::setPowerSetting(bool setting) { 195 | wantedSettings.power = lookupByteMapIndex(POWER_MAP, 2, POWER_MAP[setting ? 1 : 0]) > -1 ? POWER_MAP[setting ? 1 : 0] : POWER_MAP[0]; 196 | } 197 | 198 | const char* HeatPump::getPowerSetting() { 199 | return currentSettings.power; 200 | } 201 | 202 | void HeatPump::setPowerSetting(const char* setting) { 203 | int index = lookupByteMapIndex(POWER_MAP, 2, setting); 204 | if (index > -1) { 205 | wantedSettings.power = POWER_MAP[index]; 206 | } else { 207 | wantedSettings.power = POWER_MAP[0]; 208 | } 209 | } 210 | 211 | const char* HeatPump::getModeSetting() { 212 | return currentSettings.mode; 213 | } 214 | 215 | void HeatPump::setModeSetting(const char* setting) { 216 | int index = lookupByteMapIndex(MODE_MAP, 5, setting); 217 | if (index > -1) { 218 | wantedSettings.mode = MODE_MAP[index]; 219 | } else { 220 | wantedSettings.mode = MODE_MAP[0]; 221 | } 222 | } 223 | 224 | float HeatPump::getTemperature() { 225 | return currentSettings.temperature; 226 | } 227 | 228 | void HeatPump::setTemperature(float setting) { 229 | if(!tempMode){ 230 | wantedSettings.temperature = lookupByteMapIndex(TEMP_MAP, 16, (int)(setting + 0.5)) > -1 ? setting : TEMP_MAP[0]; 231 | } 232 | else { 233 | setting = setting * 2; 234 | setting = round(setting); 235 | setting = setting / 2; 236 | wantedSettings.temperature = setting < 10 ? 10 : (setting > 31 ? 31 : setting); 237 | } 238 | } 239 | 240 | void HeatPump::setRemoteTemperature(float setting) { 241 | byte packet[PACKET_LEN] = {}; 242 | for (int i = 0; i < 21; i++) { 243 | packet[i] = 0x00; 244 | } 245 | for (int i = 0; i < HEADER_LEN; i++) { 246 | packet[i] = HEADER[i]; 247 | } 248 | packet[5] = 0x07; 249 | if(setting > 0) { 250 | packet[6] = 0x01; 251 | setting = setting * 2; 252 | setting = round(setting); 253 | setting = setting / 2; 254 | float temp1 = 3 + ((setting - 10) * 2); 255 | packet[7] = (int)temp1; 256 | float temp2 = (setting * 2) + 128; 257 | packet[8] = (int)temp2; 258 | } 259 | else { 260 | packet[6] = 0x00; 261 | packet[8] = 0x80; //MHK1 send 80, even though it could be 00, since ControlByte is 00 262 | } 263 | // add the checksum 264 | byte chkSum = checkSum(packet, 21); 265 | packet[21] = chkSum; 266 | while(!canSend(false)) { delay(10); } 267 | writePacket(packet, PACKET_LEN); 268 | } 269 | 270 | const char* HeatPump::getFanSpeed() { 271 | return currentSettings.fan; 272 | } 273 | 274 | 275 | void HeatPump::setFanSpeed(const char* setting) { 276 | int index = lookupByteMapIndex(FAN_MAP, 6, setting); 277 | if (index > -1) { 278 | wantedSettings.fan = FAN_MAP[index]; 279 | } else { 280 | wantedSettings.fan = FAN_MAP[0]; 281 | } 282 | } 283 | 284 | const char* HeatPump::getVaneSetting() { 285 | return currentSettings.vane; 286 | } 287 | 288 | void HeatPump::setVaneSetting(const char* setting) { 289 | int index = lookupByteMapIndex(VANE_MAP, 7, setting); 290 | if (index > -1) { 291 | wantedSettings.vane = VANE_MAP[index]; 292 | } else { 293 | wantedSettings.vane = VANE_MAP[0]; 294 | } 295 | } 296 | 297 | const char* HeatPump::getWideVaneSetting() { 298 | return currentSettings.wideVane; 299 | } 300 | 301 | void HeatPump::setWideVaneSetting(const char* setting) { 302 | int index = lookupByteMapIndex(WIDEVANE_MAP, 7, setting); 303 | if (index > -1) { 304 | wantedSettings.wideVane = WIDEVANE_MAP[index]; 305 | } else { 306 | wantedSettings.wideVane = WIDEVANE_MAP[0]; 307 | } 308 | } 309 | 310 | bool HeatPump::getIseeBool() { //no setter yet 311 | return currentSettings.iSee; 312 | } 313 | 314 | heatpumpStatus HeatPump::getStatus() { 315 | return currentStatus; 316 | } 317 | 318 | float HeatPump::getRoomTemperature() { 319 | return currentStatus.roomTemperature; 320 | } 321 | 322 | bool HeatPump::getOperating() { 323 | return currentStatus.operating; 324 | } 325 | 326 | float HeatPump::FahrenheitToCelsius(int tempF) { 327 | float temp = (tempF - 32) / 1.8; 328 | return ((float)round(temp*2))/2; //Round to nearest 0.5C 329 | } 330 | 331 | int HeatPump::CelsiusToFahrenheit(float tempC) { 332 | float temp = (tempC * 1.8) + 32; //round up if heat, down if cool or any other mode 333 | return (int)(temp + 0.5); 334 | } 335 | 336 | void HeatPump::setOnConnectCallback(ON_CONNECT_CALLBACK_SIGNATURE) { 337 | this->onConnectCallback = onConnectCallback; 338 | } 339 | 340 | void HeatPump::setSettingsChangedCallback(SETTINGS_CHANGED_CALLBACK_SIGNATURE) { 341 | this->settingsChangedCallback = settingsChangedCallback; 342 | } 343 | 344 | void HeatPump::setStatusChangedCallback(STATUS_CHANGED_CALLBACK_SIGNATURE) { 345 | this->statusChangedCallback = statusChangedCallback; 346 | } 347 | 348 | void HeatPump::setPacketCallback(PACKET_CALLBACK_SIGNATURE) { 349 | this->packetCallback = packetCallback; 350 | } 351 | 352 | void HeatPump::setRoomTempChangedCallback(ROOM_TEMP_CHANGED_CALLBACK_SIGNATURE) { 353 | this->roomTempChangedCallback = roomTempChangedCallback; 354 | } 355 | 356 | //#### WARNING, THE FOLLOWING METHOD CAN F--K YOUR HP UP, USE WISELY #### 357 | void HeatPump::sendCustomPacket(byte data[], int packetLength) { 358 | while(!canSend(false)) { delay(10); } 359 | 360 | packetLength += 2; // +2 for first header byte and checksum 361 | packetLength = (packetLength > PACKET_LEN) ? PACKET_LEN : packetLength; // ensure we are not exceeding PACKET_LEN 362 | byte packet[packetLength]; 363 | packet[0] = HEADER[0]; // add first header byte 364 | 365 | // add data 366 | for (int i = 0; i < packetLength; i++) { 367 | packet[(i+1)] = data[i]; 368 | } 369 | 370 | // add checksum 371 | byte chkSum = checkSum(packet, (packetLength-1)); 372 | packet[(packetLength-1)] = chkSum; 373 | 374 | writePacket(packet, packetLength); 375 | } 376 | 377 | // Private Methods ////////////////////////////////////////////////////////////// 378 | 379 | int HeatPump::lookupByteMapIndex(const int valuesMap[], int len, int lookupValue) { 380 | for (int i = 0; i < len; i++) { 381 | if (valuesMap[i] == lookupValue) { 382 | return i; 383 | } 384 | } 385 | return -1; 386 | } 387 | 388 | int HeatPump::lookupByteMapIndex(const char* valuesMap[], int len, const char* lookupValue) { 389 | for (int i = 0; i < len; i++) { 390 | if (strcmp(valuesMap[i], lookupValue) == 0) { 391 | return i; 392 | } 393 | } 394 | return -1; 395 | } 396 | 397 | 398 | const char* HeatPump::lookupByteMapValue(const char* valuesMap[], const byte byteMap[], int len, byte byteValue) { 399 | for (int i = 0; i < len; i++) { 400 | if (byteMap[i] == byteValue) { 401 | return valuesMap[i]; 402 | } 403 | } 404 | return valuesMap[0]; 405 | } 406 | 407 | int HeatPump::lookupByteMapValue(const int valuesMap[], const byte byteMap[], int len, byte byteValue) { 408 | for (int i = 0; i < len; i++) { 409 | if (byteMap[i] == byteValue) { 410 | return valuesMap[i]; 411 | } 412 | } 413 | return valuesMap[0]; 414 | } 415 | 416 | bool HeatPump::canSend(bool isInfo) { 417 | return (millis() - (isInfo ? PACKET_INFO_INTERVAL_MS : PACKET_SENT_INTERVAL_MS)) > lastSend; 418 | } 419 | 420 | bool HeatPump::canRead() { 421 | return (waitForRead && (millis() - PACKET_SENT_INTERVAL_MS) > lastSend); 422 | } 423 | 424 | byte HeatPump::checkSum(byte bytes[], int len) { 425 | byte sum = 0; 426 | for (int i = 0; i < len; i++) { 427 | sum += bytes[i]; 428 | } 429 | return (0xfc - sum) & 0xff; 430 | } 431 | 432 | void HeatPump::createPacket(byte *packet, heatpumpSettings settings) { 433 | //preset all bytes to 0x00 434 | for (int i = 0; i < 21; i++) { 435 | packet[i] = 0x00; 436 | } 437 | for (int i = 0; i < HEADER_LEN; i++) { 438 | packet[i] = HEADER[i]; 439 | } 440 | if(settings.power != currentSettings.power) { 441 | packet[8] = POWER[lookupByteMapIndex(POWER_MAP, 2, settings.power)]; 442 | packet[6] += CONTROL_PACKET_1[0]; 443 | } 444 | if(settings.mode!= currentSettings.mode) { 445 | packet[9] = MODE[lookupByteMapIndex(MODE_MAP, 5, settings.mode)]; 446 | packet[6] += CONTROL_PACKET_1[1]; 447 | } 448 | if(!tempMode && settings.temperature!= currentSettings.temperature) { 449 | packet[10] = TEMP[lookupByteMapIndex(TEMP_MAP, 16, settings.temperature)]; 450 | packet[6] += CONTROL_PACKET_1[2]; 451 | } 452 | else if(tempMode && settings.temperature!= currentSettings.temperature) { 453 | float temp = (settings.temperature * 2) + 128; 454 | packet[19] = (int)temp; 455 | packet[6] += CONTROL_PACKET_1[2]; 456 | } 457 | if(settings.fan!= currentSettings.fan) { 458 | packet[11] = FAN[lookupByteMapIndex(FAN_MAP, 6, settings.fan)]; 459 | packet[6] += CONTROL_PACKET_1[3]; 460 | } 461 | if(settings.vane!= currentSettings.vane) { 462 | packet[12] = VANE[lookupByteMapIndex(VANE_MAP, 7, settings.vane)] | (wideVaneAdj ? 0x80 : 0x00); 463 | packet[6] += CONTROL_PACKET_1[4]; 464 | } 465 | if(settings.wideVane!= currentSettings.wideVane) { 466 | packet[18] = WIDEVANE[lookupByteMapIndex(WIDEVANE_MAP, 7, settings.wideVane)]; 467 | packet[7] += CONTROL_PACKET_2[0]; 468 | } 469 | // add the checksum 470 | byte chkSum = checkSum(packet, 21); 471 | packet[21] = chkSum; 472 | } 473 | 474 | void HeatPump::createInfoPacket(byte *packet, byte packetType) { 475 | // add the header to the packet 476 | for (int i = 0; i < INFOHEADER_LEN; i++) { 477 | packet[i] = INFOHEADER[i]; 478 | } 479 | 480 | // set the mode - settings or room temperature 481 | if(packetType != PACKET_TYPE_DEFAULT) { 482 | packet[5] = INFOMODE[packetType]; 483 | } else { 484 | // request current infoMode, and increment for the next request 485 | packet[5] = INFOMODE[infoMode]; 486 | if(infoMode == (INFOMODE_LEN - 1)) { 487 | infoMode = 0; 488 | } else { 489 | infoMode++; 490 | } 491 | } 492 | 493 | // pad the packet out 494 | for (int i = 0; i < 15; i++) { 495 | packet[i + 6] = 0x00; 496 | } 497 | 498 | // add the checksum 499 | byte chkSum = checkSum(packet, 21); 500 | packet[21] = chkSum; 501 | } 502 | 503 | void HeatPump::writePacket(byte *packet, int length) { 504 | for (int i = 0; i < length; i++) { 505 | _HardSerial->write((uint8_t)packet[i]); 506 | } 507 | 508 | if(packetCallback) { 509 | packetCallback(packet, length, (char*)"packetSent"); 510 | } 511 | waitForRead = true; 512 | lastSend = millis(); 513 | } 514 | 515 | int HeatPump::readPacket() { 516 | byte header[INFOHEADER_LEN] = {}; 517 | byte data[PACKET_LEN] = {}; 518 | bool foundStart = false; 519 | int dataSum = 0; 520 | byte checksum = 0; 521 | byte dataLength = 0; 522 | 523 | waitForRead = false; 524 | 525 | if(_HardSerial->available() > 0) { 526 | // read until we get start byte 0xfc 527 | while(_HardSerial->available() > 0 && !foundStart) { 528 | header[0] = _HardSerial->read(); 529 | if(header[0] == HEADER[0]) { 530 | foundStart = true; 531 | delay(100); // found that this delay increases accuracy when reading, might not be needed though 532 | } 533 | } 534 | 535 | if(!foundStart) { 536 | return RCVD_PKT_FAIL; 537 | } 538 | 539 | //read header 540 | for(int i=1;i<5;i++) { 541 | header[i] = _HardSerial->read(); 542 | } 543 | 544 | //check header 545 | if(header[0] == HEADER[0] && header[2] == HEADER[2] && header[3] == HEADER[3]) { 546 | dataLength = header[4]; 547 | 548 | for(int i=0;iread(); 550 | } 551 | 552 | // read checksum byte 553 | data[dataLength] = _HardSerial->read(); 554 | 555 | // sum up the header bytes... 556 | for (int i = 0; i < INFOHEADER_LEN; i++) { 557 | dataSum += header[i]; 558 | } 559 | 560 | // ...and add to that the sum of the data bytes 561 | for (int i = 0; i < dataLength; i++) { 562 | dataSum += data[i]; 563 | } 564 | 565 | // calculate checksum 566 | checksum = (0xfc - dataSum) & 0xff; 567 | 568 | if(data[dataLength] == checksum) { 569 | lastRecv = millis(); 570 | if(packetCallback) { 571 | byte packet[37]; // we are going to put header[5] and data[32] into this, so the whole packet is sent to the callback 572 | for(int i=0; i 0x08 ? true : false; 587 | receivedSettings.mode = lookupByteMapValue(MODE_MAP, MODE, 5, receivedSettings.iSee ? (data[4] - 0x08) : data[4]); 588 | 589 | if(data[11] != 0x00) { 590 | int temp = data[11]; 591 | temp -= 128; 592 | receivedSettings.temperature = (float)temp / 2; 593 | tempMode = true; 594 | } else { 595 | receivedSettings.temperature = lookupByteMapValue(TEMP_MAP, TEMP, 16, data[5]); 596 | } 597 | 598 | receivedSettings.fan = lookupByteMapValue(FAN_MAP, FAN, 6, data[6]); 599 | receivedSettings.vane = lookupByteMapValue(VANE_MAP, VANE, 7, data[7]); 600 | receivedSettings.wideVane = lookupByteMapValue(WIDEVANE_MAP, WIDEVANE, 7, data[10] & 0x0F); 601 | wideVaneAdj = (data[10] & 0xF0) == 0x80 ? true : false; 602 | 603 | if(settingsChangedCallback && receivedSettings != currentSettings) { 604 | currentSettings = receivedSettings; 605 | settingsChangedCallback(); 606 | } else { 607 | currentSettings = receivedSettings; 608 | } 609 | 610 | // if this is the first time we have synced with the heatpump, set wantedSettings to receivedSettings 611 | if(firstRun || (autoUpdate && externalUpdate)) { 612 | wantedSettings = currentSettings; 613 | firstRun = false; 614 | } 615 | 616 | return RCVD_PKT_SETTINGS; 617 | } 618 | 619 | case 0x03: { //Room temperature reading 620 | heatpumpStatus receivedStatus; 621 | 622 | if(data[6] != 0x00) { 623 | int temp = data[6]; 624 | temp -= 128; 625 | receivedStatus.roomTemperature = (float)temp / 2; 626 | } else { 627 | receivedStatus.roomTemperature = lookupByteMapValue(ROOM_TEMP_MAP, ROOM_TEMP, 32, data[3]); 628 | } 629 | 630 | if((statusChangedCallback || roomTempChangedCallback) && currentStatus.roomTemperature != receivedStatus.roomTemperature) { 631 | currentStatus.roomTemperature = receivedStatus.roomTemperature; 632 | 633 | if(statusChangedCallback) { 634 | statusChangedCallback(currentStatus); 635 | } 636 | 637 | if(roomTempChangedCallback) { // this should be deprecated - statusChangedCallback covers it 638 | roomTempChangedCallback(currentStatus.roomTemperature); 639 | } 640 | } else { 641 | currentStatus.roomTemperature = receivedStatus.roomTemperature; 642 | } 643 | 644 | return RCVD_PKT_ROOM_TEMP; 645 | } 646 | 647 | case 0x04: { // unknown 648 | break; 649 | } 650 | 651 | case 0x05: { // timer packet 652 | heatpumpTimers receivedTimers; 653 | 654 | receivedTimers.mode = lookupByteMapValue(TIMER_MODE_MAP, TIMER_MODE, 4, data[3]); 655 | receivedTimers.onMinutesSet = data[4] * TIMER_INCREMENT_MINUTES; 656 | receivedTimers.onMinutesRemaining = data[6] * TIMER_INCREMENT_MINUTES; 657 | receivedTimers.offMinutesSet = data[5] * TIMER_INCREMENT_MINUTES; 658 | receivedTimers.offMinutesRemaining = data[7] * TIMER_INCREMENT_MINUTES; 659 | 660 | // callback for status change 661 | if(statusChangedCallback && currentStatus.timers != receivedTimers) { 662 | currentStatus.timers = receivedTimers; 663 | statusChangedCallback(currentStatus); 664 | } else { 665 | currentStatus.timers = receivedTimers; 666 | } 667 | 668 | return RCVD_PKT_TIMER; 669 | } 670 | 671 | case 0x06: { // status 672 | heatpumpStatus receivedStatus; 673 | receivedStatus.operating = data[4]; 674 | receivedStatus.compressorFrequency = data[3]; 675 | 676 | // callback for status change -- not triggered for compressor frequency at the moment 677 | if(statusChangedCallback && currentStatus.operating != receivedStatus.operating) { 678 | currentStatus.operating = receivedStatus.operating; 679 | currentStatus.compressorFrequency = receivedStatus.compressorFrequency; 680 | statusChangedCallback(currentStatus); 681 | } else { 682 | currentStatus.operating = receivedStatus.operating; 683 | currentStatus.compressorFrequency = receivedStatus.compressorFrequency; 684 | } 685 | 686 | return RCVD_PKT_STATUS; 687 | } 688 | 689 | case 0x09: { // standby mode maybe? 690 | break; 691 | } 692 | } 693 | } 694 | 695 | if(header[1] == 0x61) { //Last update was successful 696 | return RCVD_PKT_UPDATE_SUCCESS; 697 | } else if(header[1] == 0x7a) { //Last update was successful 698 | connected = true; 699 | return RCVD_PKT_CONNECT_SUCCESS; 700 | } 701 | } 702 | } 703 | } 704 | 705 | return RCVD_PKT_FAIL; 706 | } 707 | 708 | -------------------------------------------------------------------------------- /libraries/HeatPump/HeatPump.h: -------------------------------------------------------------------------------- 1 | /* 2 | HeatPump.h - Mitsubishi Heat Pump control library for Arduino 3 | Copyright (c) 2017 Al Betschart. All right reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | */ 19 | #ifndef __HeatPump_H__ 20 | #define __HeatPump_H__ 21 | #include 22 | #include 23 | #include 24 | #if defined(ARDUINO) && ARDUINO >= 100 25 | #include "Arduino.h" 26 | #else 27 | #include "WProgram.h" 28 | #endif 29 | 30 | /* 31 | * Callback function definitions. Code differs for the ESP8266 platform, which requires the functional library. 32 | * Based on callback implementation in the Arduino Client for MQTT library (https://github.com/knolleary/pubsubclient) 33 | */ 34 | #ifdef ESP8266 35 | #include 36 | #define ON_CONNECT_CALLBACK_SIGNATURE std::function onConnectCallback 37 | #define SETTINGS_CHANGED_CALLBACK_SIGNATURE std::function settingsChangedCallback 38 | #define STATUS_CHANGED_CALLBACK_SIGNATURE std::function statusChangedCallback 39 | #define PACKET_CALLBACK_SIGNATURE std::function packetCallback 40 | #define ROOM_TEMP_CHANGED_CALLBACK_SIGNATURE std::function roomTempChangedCallback 41 | #else 42 | #define ON_CONNECT_CALLBACK_SIGNATURE void (*onConnectCallback)() 43 | #define SETTINGS_CHANGED_CALLBACK_SIGNATURE void (*settingsChangedCallback)() 44 | #define STATUS_CHANGED_CALLBACK_SIGNATURE void (*statusChangedCallback)(heatpumpStatus newStatus) 45 | #define PACKET_CALLBACK_SIGNATURE void (*packetCallback)(byte* packet, unsigned int length, char* packetDirection) 46 | #define ROOM_TEMP_CHANGED_CALLBACK_SIGNATURE void (*roomTempChangedCallback)(float currentRoomTemperature) 47 | #endif 48 | 49 | typedef uint8_t byte; 50 | 51 | struct heatpumpSettings { 52 | const char* power; 53 | const char* mode; 54 | float temperature; 55 | const char* fan; 56 | const char* vane; //vertical vane, up/down 57 | const char* wideVane; //horizontal vane, left/right 58 | bool iSee; //iSee sensor, at the moment can only detect it, not set it 59 | bool connected; 60 | }; 61 | 62 | bool operator==(const heatpumpSettings& lhs, const heatpumpSettings& rhs); 63 | bool operator!=(const heatpumpSettings& lhs, const heatpumpSettings& rhs); 64 | 65 | struct heatpumpTimers { 66 | const char* mode; 67 | int onMinutesSet; 68 | int onMinutesRemaining; 69 | int offMinutesSet; 70 | int offMinutesRemaining; 71 | }; 72 | 73 | bool operator==(const heatpumpTimers& lhs, const heatpumpTimers& rhs); 74 | bool operator!=(const heatpumpTimers& lhs, const heatpumpTimers& rhs); 75 | 76 | struct heatpumpStatus { 77 | float roomTemperature; 78 | bool operating; // if true, the heatpump is operating to reach the desired temperature 79 | heatpumpTimers timers; 80 | int compressorFrequency; 81 | }; 82 | 83 | class HeatPump 84 | { 85 | private: 86 | static const int PACKET_LEN = 22; 87 | static const int PACKET_SENT_INTERVAL_MS = 1000; 88 | static const int PACKET_INFO_INTERVAL_MS = 2000; 89 | static const int PACKET_TYPE_DEFAULT = 99; 90 | 91 | static const int CONNECT_LEN = 8; 92 | const byte CONNECT[CONNECT_LEN] = {0xfc, 0x5a, 0x01, 0x30, 0x02, 0xca, 0x01, 0xa8}; 93 | static const int HEADER_LEN = 8; 94 | const byte HEADER[HEADER_LEN] = {0xfc, 0x41, 0x01, 0x30, 0x10, 0x01, 0x00, 0x00}; 95 | 96 | static const int INFOHEADER_LEN = 5; 97 | const byte INFOHEADER[INFOHEADER_LEN] = {0xfc, 0x42, 0x01, 0x30, 0x10}; 98 | 99 | 100 | static const int INFOMODE_LEN = 6; 101 | const byte INFOMODE[INFOMODE_LEN] = { 102 | 0x02, // request a settings packet - RQST_PKT_SETTINGS 103 | 0x03, // request the current room temp - RQST_PKT_ROOM_TEMP 104 | 0x04, // unknown 105 | 0x05, // request the timers - RQST_PKT_TIMERS 106 | 0x06, // request status - RQST_PKT_STATUS 107 | 0x09 // request standby mode (maybe?) RQST_PKT_STANDBY 108 | }; 109 | 110 | const int RCVD_PKT_FAIL = 0; 111 | const int RCVD_PKT_CONNECT_SUCCESS = 1; 112 | const int RCVD_PKT_SETTINGS = 2; 113 | const int RCVD_PKT_ROOM_TEMP = 3; 114 | const int RCVD_PKT_UPDATE_SUCCESS = 4; 115 | const int RCVD_PKT_STATUS = 5; 116 | const int RCVD_PKT_TIMER = 6; 117 | 118 | const byte CONTROL_PACKET_1[5] = {0x01, 0x02, 0x04, 0x08, 0x10}; 119 | //{"POWER","MODE","TEMP","FAN","VANE"}; 120 | const byte CONTROL_PACKET_2[1] = {0x01}; 121 | //{"WIDEVANE"}; 122 | const byte POWER[2] = {0x00, 0x01}; 123 | const char* POWER_MAP[2] = {"OFF", "ON"}; 124 | const byte MODE[5] = {0x01, 0x02, 0x03, 0x07, 0x08}; 125 | const char* MODE_MAP[5] = {"HEAT", "DRY", "COOL", "FAN", "AUTO"}; 126 | const byte TEMP[16] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; 127 | const int TEMP_MAP[16] = {31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16}; 128 | const byte FAN[6] = {0x00, 0x01, 0x02, 0x03, 0x05, 0x06}; 129 | const char* FAN_MAP[6] = {"AUTO", "QUIET", "1", "2", "3", "4"}; 130 | const byte VANE[7] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x07}; 131 | const char* VANE_MAP[7] = {"AUTO", "1", "2", "3", "4", "5", "SWING"}; 132 | const byte WIDEVANE[7] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x08, 0x0c}; 133 | const char* WIDEVANE_MAP[7] = {"<<", "<", "|", ">", ">>", "<>", "SWING"}; 134 | const byte ROOM_TEMP[32] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 135 | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f}; 136 | const int ROOM_TEMP_MAP[32] = {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 137 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41}; 138 | const byte TIMER_MODE[4] = {0x00, 0x01, 0x02, 0x03}; 139 | const char* TIMER_MODE_MAP[4] = {"NONE", "OFF", "ON", "BOTH"}; 140 | 141 | static const int TIMER_INCREMENT_MINUTES = 10; 142 | 143 | // these settings will be initialised in connect() 144 | heatpumpSettings currentSettings; 145 | heatpumpSettings wantedSettings; 146 | 147 | heatpumpStatus currentStatus; 148 | 149 | HardwareSerial * _HardSerial; 150 | unsigned long lastSend; 151 | bool waitForRead; 152 | int infoMode; 153 | unsigned long lastRecv; 154 | bool connected = false; 155 | bool autoUpdate; 156 | bool firstRun; 157 | bool tempMode; 158 | bool externalUpdate; 159 | bool wideVaneAdj; 160 | int bitrate = 2400; 161 | 162 | const char* lookupByteMapValue(const char* valuesMap[], const byte byteMap[], int len, byte byteValue); 163 | int lookupByteMapValue(const int valuesMap[], const byte byteMap[], int len, byte byteValue); 164 | int lookupByteMapIndex(const char* valuesMap[], int len, const char* lookupValue); 165 | int lookupByteMapIndex(const int valuesMap[], int len, int lookupValue); 166 | 167 | bool canSend(bool isInfo); 168 | bool canRead(); 169 | byte checkSum(byte bytes[], int len); 170 | void createPacket(byte *packet, heatpumpSettings settings); 171 | void createInfoPacket(byte *packet, byte packetType); 172 | int readPacket(); 173 | void writePacket(byte *packet, int length); 174 | 175 | // callbacks 176 | ON_CONNECT_CALLBACK_SIGNATURE; 177 | SETTINGS_CHANGED_CALLBACK_SIGNATURE; 178 | STATUS_CHANGED_CALLBACK_SIGNATURE; 179 | PACKET_CALLBACK_SIGNATURE; 180 | ROOM_TEMP_CHANGED_CALLBACK_SIGNATURE; 181 | 182 | public: 183 | // indexes for INFOMODE array (public so they can be optionally passed to sync()) 184 | const int RQST_PKT_SETTINGS = 0; 185 | const int RQST_PKT_ROOM_TEMP = 1; 186 | const int RQST_PKT_TIMERS = 3; 187 | const int RQST_PKT_STATUS = 4; 188 | const int RQST_PKT_STANDBY = 5; 189 | 190 | // general 191 | HeatPump(); 192 | bool connect(HardwareSerial *serial); 193 | bool connect(HardwareSerial *serial, bool retry); 194 | bool update(); 195 | void sync(byte packetType = PACKET_TYPE_DEFAULT); 196 | void enableExternalUpdate(); 197 | void enableAutoUpdate(); 198 | void disableAutoUpdate(); 199 | 200 | // settings 201 | heatpumpSettings getSettings(); 202 | void setSettings(heatpumpSettings settings); 203 | void setPowerSetting(bool setting); 204 | bool getPowerSettingBool(); 205 | const char* getPowerSetting(); 206 | void setPowerSetting(const char* setting); 207 | const char* getModeSetting(); 208 | void setModeSetting(const char* setting); 209 | float getTemperature(); 210 | void setTemperature(float setting); 211 | void setRemoteTemperature(float setting); 212 | const char* getFanSpeed(); 213 | void setFanSpeed(const char* setting); 214 | const char* getVaneSetting(); 215 | void setVaneSetting(const char* setting); 216 | const char* getWideVaneSetting(); 217 | void setWideVaneSetting(const char* setting); 218 | bool getIseeBool(); 219 | 220 | // status 221 | heatpumpStatus getStatus(); 222 | float getRoomTemperature(); 223 | bool getOperating(); 224 | bool isConnected(); 225 | 226 | // helpers 227 | float FahrenheitToCelsius(int tempF); 228 | int CelsiusToFahrenheit(float tempC); 229 | 230 | // callbacks 231 | void setOnConnectCallback(ON_CONNECT_CALLBACK_SIGNATURE); 232 | void setSettingsChangedCallback(SETTINGS_CHANGED_CALLBACK_SIGNATURE); 233 | void setStatusChangedCallback(STATUS_CHANGED_CALLBACK_SIGNATURE); 234 | void setPacketCallback(PACKET_CALLBACK_SIGNATURE); 235 | void setRoomTempChangedCallback(ROOM_TEMP_CHANGED_CALLBACK_SIGNATURE); // need to deprecate this, is available from setStatusChangedCallback 236 | 237 | // expert users only! 238 | void sendCustomPacket(byte data[], int len); 239 | 240 | }; 241 | #endif 242 | -------------------------------------------------------------------------------- /libraries/README.md: -------------------------------------------------------------------------------- 1 | # Libraries 2 | 3 | Included libraries for compatability and convenience. 4 | 5 | ## HeatPump 6 | https://github.com/SwiCago/HeatPump 7 | -------------------------------------------------------------------------------- /mitsucon.h: -------------------------------------------------------------------------------- 1 | // Home Assistant Mitsubishi Electric Heat Pump Controller https://github.com/unixko/MitsuCon 2 | // using native MQTT Climate (HVAC) component with MQTT discovery for automatic configuration 3 | // Set PubSubClient.h MQTT_MAX_PACKET_SIZE to 2048 4 | 5 | // enable extra MQTT topic for debug/timer info 6 | bool _debugMode = false; 7 | bool _timersAttr = false; 8 | 9 | // comment out to disable OTA 10 | #define OTA 11 | const char* ota_password = "OTA_PASSWORD"; 12 | 13 | // wifi settings 14 | const char* ssid = "WIFI_SSID"; 15 | const char* password = "WIFI_PASSWORD"; 16 | 17 | // mqtt server settings 18 | const char* mqtt_server = "MQTT_SERVER_NAME_OR_IP"; 19 | const int mqtt_port = 1883; 20 | const char* mqtt_username = "MQTT_USER"; 21 | const char* mqtt_password = "MQTT_PASSWORD"; 22 | 23 | // mqtt client settings 24 | // Change "heatpump" to be same on all lines 25 | const char* name = "Heat Pump"; // Device Name displayed in Home Assistant 26 | const char* client_id = "heatpump"; // WiFi hostname, OTA hostname, MQTT hostname 27 | const char* heatpump_topic = "heatpump"; // MQTT topic, must be unique between heat pump unit 28 | const char* heatpump_availability_topic = "heatpump/tele/avty"; 29 | const char* heatpump_state_topic = "heatpump/tele/stat"; 30 | const char* heatpump_current_topic = "heatpump/tele/curr"; 31 | const char* heatpump_attribute_topic = "heatpump/tele/attr"; 32 | const char* heatpump_mode_command_topic = "heatpump/cmnd/mode"; 33 | const char* heatpump_temperature_command_topic = "heatpump/cmnd/temp"; 34 | const char* heatpump_fan_mode_command_topic = "heatpump/cmnd/fan"; 35 | const char* heatpump_swing_mode_command_topic = "heatpump/cmnd/vane"; 36 | const char* heatpump_debug_topic = "heatpump/debug"; 37 | 38 | // Customization 39 | const char* min_temp = "16"; // Minimum temperature, check value from heatpump remote control 40 | const char* max_temp = "31"; // Maximum temperature, check value from heatpump remote control 41 | const char* temp_step = "1"; // Temperature setting step, check value from heatpump remote control 42 | const char* mqtt_discov_prefix = "homeassistant"; // Home Assistant MQTT Discovery Prefix 43 | 44 | // pinouts 45 | const int redLedPin = 0; // Onboard LED = digital pin 0 (red LED on adafruit ESP8266 huzzah) 46 | const int blueLedPin = 2; // Onboard LED = digital pin 0 (blue LED on adafruit ESP8266 huzzah) 47 | 48 | // sketch settings 49 | const unsigned int SEND_ROOM_TEMP_INTERVAL_MS = 60000; 50 | -------------------------------------------------------------------------------- /mitsucon.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "mitsucon.h" 8 | 9 | #ifdef OTA 10 | #include 11 | #include 12 | #endif 13 | 14 | WiFiClient espClient; 15 | PubSubClient mqtt_client(espClient); 16 | HeatPump hp; 17 | unsigned long lastTempSend; 18 | 19 | const char* controller_sw_version = "20200429"; 20 | 21 | void mqttConnect() { 22 | while (!mqtt_client.connected()) { 23 | if (mqtt_client.connect(client_id, mqtt_username, mqtt_password, heatpump_availability_topic, 1, 1, "offline")) { 24 | mqtt_client.subscribe(heatpump_mode_command_topic); 25 | mqtt_client.subscribe(heatpump_temperature_command_topic); 26 | mqtt_client.subscribe(heatpump_fan_mode_command_topic); 27 | mqtt_client.subscribe(heatpump_swing_mode_command_topic); 28 | mqtt_client.publish(heatpump_availability_topic, "online", true); 29 | } else { 30 | delay(5000); 31 | } 32 | } 33 | } 34 | 35 | void mqttAutoDiscovery() { 36 | const String chip_id = String(ESP.getChipId()); 37 | const String mqtt_discov_topic = String(mqtt_discov_prefix) + "/climate/" + chip_id + "/config"; 38 | 39 | const size_t bufferSizeDiscovery = JSON_OBJECT_SIZE(60); 40 | DynamicJsonDocument rootDiscovery(bufferSizeDiscovery); 41 | 42 | rootDiscovery["name"] = name; 43 | rootDiscovery["uniq_id"] = chip_id; 44 | rootDiscovery["~"] = heatpump_topic; 45 | rootDiscovery["min_temp"] = min_temp; 46 | rootDiscovery["max_temp"] = max_temp; 47 | rootDiscovery["temp_step"] = temp_step; 48 | rootDiscovery["temperature_unit"] = "C"; 49 | JsonArray modes = rootDiscovery.createNestedArray("modes"); 50 | modes.add("heat_cool"); 51 | modes.add("cool"); 52 | modes.add("dry"); 53 | modes.add("heat"); 54 | modes.add("fan_only"); 55 | modes.add("off"); 56 | JsonArray fan_modes = rootDiscovery.createNestedArray("fan_modes"); 57 | fan_modes.add("auto"); 58 | fan_modes.add("quiet"); 59 | fan_modes.add("1"); 60 | fan_modes.add("2"); 61 | fan_modes.add("3"); 62 | fan_modes.add("4"); 63 | JsonArray swing_modes = rootDiscovery.createNestedArray("swing_modes"); 64 | swing_modes.add("auto"); 65 | swing_modes.add("1"); 66 | swing_modes.add("2"); 67 | swing_modes.add("3"); 68 | swing_modes.add("4"); 69 | swing_modes.add("5"); 70 | swing_modes.add("swing"); 71 | rootDiscovery["avty_t"] = "~/tele/avty"; 72 | rootDiscovery["curr_temp_t"] = "~/tele/curr"; 73 | rootDiscovery["curr_temp_tpl"] = "{{ value_json.roomTemperature }}"; 74 | rootDiscovery["mode_cmd_t"] = "~/cmnd/mode"; 75 | rootDiscovery["mode_stat_t"] = "~/tele/stat"; 76 | rootDiscovery["mode_stat_tpl"] = "{{ 'off' if value_json.power == 'OFF' else value_json.mode | lower | replace('auto', 'heat_cool') | replace('fan', 'fan_only') }}"; 77 | rootDiscovery["temp_cmd_t"] = "~/cmnd/temp"; 78 | rootDiscovery["temp_stat_t"] = "~/tele/stat"; 79 | rootDiscovery["temp_stat_tpl"] = "{{ value_json.temperature }}"; 80 | rootDiscovery["fan_mode_cmd_t"] = "~/cmnd/fan"; 81 | rootDiscovery["fan_mode_stat_t"] = "~/tele/stat"; 82 | rootDiscovery["fan_mode_stat_tpl"] = "{{ value_json.fan | lower }}"; 83 | rootDiscovery["swing_mode_cmd_t"] = "~/cmnd/vane"; 84 | rootDiscovery["swing_mode_stat_t"] = "~/tele/stat"; 85 | rootDiscovery["swing_mode_stat_tpl"] = "{{ value_json.vane | lower }}"; 86 | rootDiscovery["act_t"] = "~/tele/curr"; 87 | rootDiscovery["act_tpl"] = "{%set hp='climate.'+value_json.name|lower|replace(' ','_')%}{%if states(hp)=='off'%}off{%elif states(hp)=='fan'%}fan{%elif value_json.operating==false%}idle{%elif states(hp)=='heat'%}heating{%elif states(hp)=='cool' %}cooling{%elif states(hp)=='dry' %}drying{%elif states(hp)=='heat_cool'and(state_attr(hp,'temperature')-state_attr(hp,'current_temperature')>0)%}heating{%elif states(hp)=='heat_cool'and(state_attr(hp,'temperature')-state_attr(hp,'current_temperature')<0)%}cooling{%endif%}"; 88 | #ifdef _timersAttr 89 | rootDiscovery["json_attr_t"] = "~/tele/attr"; 90 | #endif 91 | JsonObject device = rootDiscovery.createNestedObject("device"); 92 | device["name"] = name; 93 | JsonArray ids = device.createNestedArray("ids"); 94 | ids.add(chip_id); 95 | device["mf"] = "MitsuCon"; 96 | device["mdl"] = "Mitsubishi Electric Heat Pump"; 97 | device["sw"] = controller_sw_version; 98 | 99 | char bufferDiscovery[2048]; 100 | serializeJson(rootDiscovery, bufferDiscovery); 101 | 102 | if (!mqtt_client.publish(mqtt_discov_topic.c_str(), bufferDiscovery, true)) { 103 | mqtt_client.publish(heatpump_debug_topic, "failed to publish DISCOV topic"); 104 | } 105 | } 106 | 107 | void hpSettingsChanged() { 108 | const size_t bufferSize = JSON_OBJECT_SIZE(6); 109 | DynamicJsonDocument root(bufferSize); 110 | 111 | heatpumpSettings currentSettings = hp.getSettings(); 112 | 113 | root["power"] = currentSettings.power; 114 | root["mode"] = currentSettings.mode; 115 | root["temperature"] = currentSettings.temperature; 116 | root["fan"] = currentSettings.fan; 117 | root["vane"] = currentSettings.vane; 118 | root["wideVane"] = currentSettings.wideVane; 119 | 120 | char buffer[512]; 121 | serializeJson(root, buffer); 122 | 123 | if (!mqtt_client.publish(heatpump_state_topic, buffer, true)) { 124 | mqtt_client.publish(heatpump_debug_topic, "failed to publish STATE topic"); 125 | } 126 | } 127 | 128 | void hpStatusChanged(heatpumpStatus currentStatus) { 129 | const size_t bufferSizeInfo = JSON_OBJECT_SIZE(4); 130 | DynamicJsonDocument rootInfo(bufferSizeInfo); 131 | 132 | rootInfo["name"] = name; 133 | rootInfo["roomTemperature"] = currentStatus.roomTemperature; 134 | rootInfo["operating"] = currentStatus.operating; 135 | 136 | char bufferInfo[512]; 137 | serializeJson(rootInfo, bufferInfo); 138 | 139 | if (!mqtt_client.publish(heatpump_current_topic, bufferInfo, true)) { 140 | mqtt_client.publish(heatpump_debug_topic, "failed to publish CURR topic"); 141 | } 142 | 143 | #ifdef _timersAttr 144 | const size_t bufferSizeTimers = JSON_OBJECT_SIZE(5); 145 | DynamicJsonDocument rootTimers(bufferSizeTimers); 146 | 147 | 148 | rootTimers["timer_set"] = currentStatus.timers.mode; 149 | rootTimers["timer_on_mins"] = currentStatus.timers.onMinutesSet; 150 | rootTimers["timer_on_remain"] = currentStatus.timers.onMinutesRemaining; 151 | rootTimers["timer_off_mins"] = currentStatus.timers.offMinutesSet; 152 | rootTimers["timer_off_remain"] = currentStatus.timers.offMinutesRemaining; 153 | 154 | char bufferTimers[512]; 155 | serializeJson(rootTimers, bufferTimers); 156 | 157 | if (!mqtt_client.publish(heatpump_attribute_topic, bufferTimers, true)) { 158 | mqtt_client.publish(heatpump_debug_topic, "failed to publish ATTR topic"); 159 | } 160 | #endif 161 | 162 | mqtt_client.publish(heatpump_availability_topic, "online", true); 163 | } 164 | 165 | void hpPacketDebug(byte* packet, unsigned int length, char* packetDirection) { 166 | if (_debugMode) { 167 | String message; 168 | for (int idx = 0; idx < length; idx++) { 169 | if (packet[idx] < 16) { 170 | message += "0"; 171 | } 172 | message += String(packet[idx], HEX) + " "; 173 | } 174 | 175 | const size_t bufferSize = JSON_OBJECT_SIZE(8); 176 | DynamicJsonDocument root(bufferSize); 177 | 178 | root[packetDirection] = message; 179 | 180 | char buffer[512]; 181 | serializeJson(root, buffer); 182 | 183 | if (!mqtt_client.publish(heatpump_debug_topic, buffer)) { 184 | mqtt_client.publish(heatpump_debug_topic, "failed to publish DEBUG topic"); 185 | } 186 | } 187 | } 188 | 189 | void mqttCallback(char* topic, byte* payload, unsigned int length) { 190 | char message[length + 1]; 191 | for (int i = 0; i < length; i++) { 192 | message[i] = (char)payload[i]; 193 | } 194 | message[length] = '\0'; 195 | 196 | const size_t bufferSize = JSON_OBJECT_SIZE(6); 197 | DynamicJsonDocument root(bufferSize); 198 | 199 | heatpumpSettings currentSettings = hp.getSettings(); 200 | 201 | root["power"] = currentSettings.power; 202 | root["mode"] = currentSettings.mode; 203 | root["temperature"] = currentSettings.temperature; 204 | root["fan"] = currentSettings.fan; 205 | root["vane"] = currentSettings.vane; 206 | 207 | if (strcmp(topic, heatpump_mode_command_topic) == 0) { 208 | if (strcmp(message, "off") == 0) { 209 | const char* power = "OFF"; 210 | hp.setPowerSetting(power); 211 | root["power"] = power; 212 | } else if (strcmp(message, "heat_cool") == 0) { 213 | const char* power = "ON"; 214 | hp.setPowerSetting(power); 215 | root["power"] = power; 216 | const char* mode = "AUTO"; 217 | hp.setModeSetting(mode); 218 | root["mode"] = mode; 219 | } else if (strcmp(message, "fan_only") == 0) { 220 | const char* power = "ON"; 221 | hp.setPowerSetting(power); 222 | root["power"] = power; 223 | const char* mode = "FAN"; 224 | hp.setModeSetting(mode); 225 | root["mode"] = mode; 226 | } else { 227 | const char* power = "ON"; 228 | hp.setPowerSetting(power); 229 | root["power"] = power; 230 | const char* mode = strupr(message); 231 | hp.setModeSetting(mode); 232 | root["mode"] = mode; 233 | } 234 | } else if (strcmp(topic, heatpump_temperature_command_topic) == 0) { 235 | float temperature = atof(message); 236 | hp.setTemperature(temperature); 237 | root["temperature"] = temperature; 238 | } else if (strcmp(topic, heatpump_fan_mode_command_topic) == 0) { 239 | const char* fan = strupr(message); 240 | hp.setFanSpeed(fan); 241 | root["fan"] = fan; 242 | } else if (strcmp(topic, heatpump_swing_mode_command_topic) == 0) { 243 | const char* vane = strupr(message); 244 | hp.setVaneSetting(vane); 245 | root["vane"] = vane; 246 | } 247 | 248 | hp.update(); 249 | 250 | char buffer[512]; 251 | serializeJson(root, buffer); 252 | 253 | if (!mqtt_client.publish(heatpump_state_topic, buffer, true)) { 254 | mqtt_client.publish(heatpump_debug_topic, "failed to publish STATE topic"); 255 | } 256 | } 257 | 258 | void setup() { 259 | pinMode(redLedPin, OUTPUT); 260 | digitalWrite(redLedPin, HIGH); 261 | pinMode(blueLedPin, OUTPUT); 262 | digitalWrite(blueLedPin, HIGH); 263 | 264 | WiFi.hostname(client_id); 265 | WiFi.mode(WIFI_STA); 266 | WiFi.begin(ssid, password); 267 | 268 | while (WiFi.status() != WL_CONNECTED) { 269 | digitalWrite(blueLedPin, LOW); 270 | delay(250); 271 | digitalWrite(blueLedPin, HIGH); 272 | delay(250); 273 | } 274 | 275 | mqtt_client.setServer(mqtt_server, mqtt_port); 276 | mqtt_client.setCallback(mqttCallback); 277 | mqttConnect(); 278 | 279 | hp.setSettingsChangedCallback(hpSettingsChanged); 280 | hp.setStatusChangedCallback(hpStatusChanged); 281 | hp.setPacketCallback(hpPacketDebug); 282 | 283 | #ifdef OTA 284 | ArduinoOTA.setHostname(client_id); 285 | ArduinoOTA.setPassword(ota_password); 286 | ArduinoOTA.begin(); 287 | #endif 288 | 289 | hp.connect(&Serial); 290 | 291 | mqttAutoDiscovery(); 292 | 293 | lastTempSend = millis(); 294 | } 295 | 296 | void loop() { 297 | if (!mqtt_client.connected()) { 298 | mqttConnect(); 299 | } 300 | 301 | hp.sync(); 302 | 303 | if (millis() > (lastTempSend + SEND_ROOM_TEMP_INTERVAL_MS)) { 304 | hpStatusChanged(hp.getStatus()); 305 | lastTempSend = millis(); 306 | } 307 | 308 | mqtt_client.loop(); 309 | 310 | #ifdef OTA 311 | ArduinoOTA.handle(); 312 | #endif 313 | } 314 | --------------------------------------------------------------------------------