From b757279401bd46bd84b19e6e3fbfc5d772ce3df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Mon, 8 Mar 2021 15:54:18 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 + custom_components/toshiba/__init__.py | 0 custom_components/toshiba/climate.py | 30 ++++ custom_components/toshiba/toshiba.cpp | 225 ++++++++++++++++++++++++++ custom_components/toshiba/toshiba.h | 51 ++++++ heatpump.yaml | 72 +++++++++ 6 files changed, 381 insertions(+) create mode 100644 .gitignore create mode 100755 custom_components/toshiba/__init__.py create mode 100755 custom_components/toshiba/climate.py create mode 100755 custom_components/toshiba/toshiba.cpp create mode 100755 custom_components/toshiba/toshiba.h create mode 100755 heatpump.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d9fc1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/*/ +!custom_components/ +**/__pycache__/ \ No newline at end of file diff --git a/custom_components/toshiba/__init__.py b/custom_components/toshiba/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/custom_components/toshiba/climate.py b/custom_components/toshiba/climate.py new file mode 100755 index 0000000..96925e9 --- /dev/null +++ b/custom_components/toshiba/climate.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, remote_transmitter, sensor +from esphome.const import CONF_ID, CONF_SENSOR + +AUTO_LOAD = ['sensor'] + +toshiba_ns = cg.esphome_ns.namespace('toshiba') +ToshibaClimate = toshiba_ns.class_('ToshibaClimate', climate.Climate, cg.Component) + +CONF_TRANSMITTER_ID = 'transmitter_id' + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ToshibaClimate), + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), +}).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + if CONF_SENSOR in config: + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + + transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter)) diff --git a/custom_components/toshiba/toshiba.cpp b/custom_components/toshiba/toshiba.cpp new file mode 100755 index 0000000..688e79f --- /dev/null +++ b/custom_components/toshiba/toshiba.cpp @@ -0,0 +1,225 @@ +#include "toshiba.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace toshiba { + +static const char *TAG = "toshiba.climate"; + +const uint32_t TOSHIBA_HDR_MARK = 4400; +const uint32_t TOSHIBA_HDR_SPACE = 4400; +const uint32_t TOSHIBA_BIT_MARK = 550; +const uint32_t TOSHIBA_ONE_SPACE = 1600; +const uint32_t TOSHIBA_ZERO_SPACE = 550; + +const uint8_t TOSHIBA_FAN_AUTO = 0x00; +const uint8_t TOSHIBA_MODE_OFF = 0xE0; +const uint8_t TOSHIBA_MODE_AUTO = 0x00; +const uint8_t TOSHIBA_MODE_HEAT = 0xC0; +const uint8_t TOSHIBA_MODE_COOL = 0x80; +const uint8_t TOSHIBA_MODE_DRY = 0x40; +const uint8_t TOSHIBA_FAN1 = 0x02; +const uint8_t TOSHIBA_FAN2 = 0x06; +const uint8_t TOSHIBA_FAN3 = 0x01; +const uint8_t TOSHIBA_FAN4 = 0x05; +const uint8_t TOSHIBA_FAN5 = 0x03; +const uint8_t TOSHIBA_PURIFIER = 0x08; + +const uint8_t TOSHIBA_TEMP_MIN = 17; +const uint8_t TOSHIBA_TEMP_MAX = 30; + + +climate::ClimateTraits ToshibaClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_auto_mode(true); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_away(false); + traits.set_visual_min_temperature(TOSHIBA_TEMP_MIN); + traits.set_visual_max_temperature(TOSHIBA_TEMP_MAX); + traits.set_visual_temperature_step(1); + return traits; +} + + // TEMP MODE CHECK + // Off 0x4f 0xb0 0xc0 0x3f 0x80 0x06 0xe0 0x00 0x66 + // 23 A H 0x4f 0xb0 0xc0 0x3f 0x80 0x06 0xc0 0x00 0x46 + // AUTO 0x4f 0xb0 0xc0 0x3f 0x80 0x06 0x00 0x00 0x86 + // Pure 0x4f 0xb0 0xc0 0x3f 0x80 0x06 0x00 0x08 0x8e + // + // High power 0x4f 0xb0 0x20 0xdf 0x90 0x06 0xc0 0x00 0x80 0xd6 + // Sleep mode 0x4f 0xb0 0x20 0xdf 0x90 0x06 0xc0 0x00 0xc0 0x96 + // Fix 0x4f 0xb0 0x80 0x7f 0x84 0x00 0x84 + // Swing 0x4f 0xb0 0x80 0x7f 0x84 0x20 0xa4 +void ToshibaClimate::fix_louvre() { + /* + * Fix + * 0x4f, 0xb0, 0x80, 0x7f, 0x84, 0x00, 0x84 + */ + + ESP_LOGI(TAG, "SEND FIX COMMAND"); + uint8_t remote_state[] = { 0x4F, 0xB0, 0x80, 0x7F, 0x84, 0x00, 0x84 }; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(38000); + + data->mark(TOSHIBA_HDR_MARK); + data->space(TOSHIBA_HDR_SPACE); + for (uint8_t i : remote_state) + for (uint8_t j = 0; j < 8; j++) { + data->mark(TOSHIBA_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? TOSHIBA_ONE_SPACE : TOSHIBA_ZERO_SPACE); + } + + data->mark(TOSHIBA_BIT_MARK); + data->space(0); + transmit.perform(); +} +void ToshibaClimate::swing_louvre() { + /* + * Swing + * 0x4f, 0xb0, 0x80, 0x7f, 0x84, 0x20, 0xa4 + */ + ESP_LOGI(TAG, "SEND SWING COMMAND"); + uint8_t remote_state[] = { 0x4F, 0xB0, 0x80, 0x7F, 0x84, 0x20, 0xA4 }; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(38000); + + data->mark(TOSHIBA_HDR_MARK); + data->space(TOSHIBA_HDR_SPACE); + for (uint8_t i : remote_state) + for (uint8_t j = 0; j < 8; j++) { + data->mark(TOSHIBA_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? TOSHIBA_ONE_SPACE : TOSHIBA_ZERO_SPACE); + } + + data->mark(TOSHIBA_BIT_MARK); + data->space(0); + transmit.perform(); +} + +void ToshibaClimate::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_AUTO; + // initialize target temperature to some value so that it's not NAN + this->target_temperature = 23; + } + +} + + +void ToshibaClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + this->transmit_state_(); + this->publish_state(); +} + +uint8_t reverse_byte(uint8_t in) { + const uint8_t BITS = 8; + uint8_t out = 0; + for(int i = 0; i < BITS; i++) { + if(in & (1 << i)) { + out |= (1 << (BITS-1-i)); + } + } + return out; +} + +void ToshibaClimate::transmit_state_() { + uint8_t operating_mode; + uint8_t fan_speed = TOSHIBA_FAN_AUTO; + uint8_t temperature = 23; + uint8_t purifier = 0; + if (this->pure_) { + purifier = TOSHIBA_PURIFIER; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + operating_mode = TOSHIBA_MODE_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + operating_mode = TOSHIBA_MODE_COOL; + break; + case climate::CLIMATE_MODE_AUTO: + operating_mode = TOSHIBA_MODE_AUTO; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = TOSHIBA_MODE_OFF; + break; + } + + temperature = (uint8_t) roundf(clamp(this->target_temperature, TOSHIBA_TEMP_MIN, TOSHIBA_TEMP_MAX)); + + uint8_t temperatures[] = { + 0x00, 0x08, 0x04, 0x0c, 0x02, 0x0a, 0x06, + 0x0e, 0x01, 0x09, 0x05, 0x0d, 0x03, 0x0b + }; + + uint8_t remote_state[] = { + 0x4F, 0xB0, 0xC0, 0x3F, 0x80, + (uint8_t)(temperatures[temperature - TOSHIBA_TEMP_MIN]), + (uint8_t)(operating_mode | fan_speed), + (uint8_t)(purifier), + 0x00 + }; + + + uint8_t checksum{0}; + for (uint8_t i = 0; i < 8; i++) { + checksum ^= reverse_byte(remote_state[i]); + } + remote_state[8] = reverse_byte(checksum); + + ESP_LOGV(TAG, "Sending toshiba code: %u", remote_state); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(TOSHIBA_HDR_MARK); + data->space(TOSHIBA_HDR_SPACE); + // Data + for (uint8_t i : remote_state) + for (uint8_t j = 0; j < 8; j++) { + data->mark(TOSHIBA_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? TOSHIBA_ONE_SPACE : TOSHIBA_ZERO_SPACE); + } + // End mark + data->mark(TOSHIBA_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +} // namespace toshiba +} // namespace esphome diff --git a/custom_components/toshiba/toshiba.h b/custom_components/toshiba/toshiba.h new file mode 100755 index 0000000..2ef7f34 --- /dev/null +++ b/custom_components/toshiba/toshiba.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace toshiba { + + +class ToshibaClimate : public climate::Climate, public Component { + public: + + void setup() override; + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + + void fix_louvre(); + void swing_louvre(); + + void set_purifier(bool mode) {this->pure_ = mode; this->transmit_state_();} + bool get_purifier() {return this->pure_;} + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Transmit via IR the state of this climate controller. + void transmit_state_(); + + bool supports_cool_{true}; + bool supports_heat_{true}; + bool pure_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace toshiba +} // namespace esphome + diff --git a/heatpump.yaml b/heatpump.yaml new file mode 100755 index 0000000..eedb23e --- /dev/null +++ b/heatpump.yaml @@ -0,0 +1,72 @@ +# https://github.com/glmnet/esphome/blob/climate-toshiba/esphome/components/toshiba +esphome: + name: heatpump + platform: ESP8266 + board: nodemcuv2 + +wifi: + ssid: "XXXX" + password: "XXXX" + fast_connect: true + output_power: 15 + manual_ip: + static_ip: 192.168.2.132 + gateway: 192.168.2.1 + subnet: 255.255.255.0 + + ap: + ssid: "Heatpump Fallback Hotspot" + password: "XXXX" + +captive_portal: +logger: +api: +ota: + +dallas: + - pin: D6 + +sensor: + - platform: dallas + address: 0x990000071E28AD28 + name: Current temperature + internal: true + id: temp_sensor + +switch: + - platform: gpio + name: LED + pin: + number: D4 + inverted: yes + - platform: template + name: Fix Louvre + lambda: return false; + turn_on_action: + - lambda: |- + id(heatpump).fix_louvre(); + - platform: template + name: Swing Louvre + lambda: return false; + turn_on_action: + - lambda: |- + id(heatpump).swing_louvre(); + - platform: template + name: Air purifier + lambda: |- + return id(heatpump).get_purifier(); + turn_on_action: + - lambda: |- + id(heatpump).set_purifier(true); + turn_off_action: + - lambda: |- + id(heatpump).set_purifier(false); + +remote_transmitter: + pin: D7 + carrier_duty_percent: 50% +climate: + - platform: toshiba + name: Varmepump + id: heatpump + sensor: temp_sensor