From dc9f304d94811841bd1b17c12eb55fcba7ebd00c Mon Sep 17 00:00:00 2001 From: Mario Di Raimondo Date: Sat, 15 Jun 2019 18:00:55 +0200 Subject: [PATCH] Add Yashima climate component (#634) --- esphome/components/yashima/__init__.py | 0 esphome/components/yashima/climate.py | 36 +++++ esphome/components/yashima/yashima.cpp | 193 +++++++++++++++++++++++++ esphome/components/yashima/yashima.h | 40 +++++ 4 files changed, 269 insertions(+) create mode 100644 esphome/components/yashima/__init__.py create mode 100644 esphome/components/yashima/climate.py create mode 100644 esphome/components/yashima/yashima.cpp create mode 100644 esphome/components/yashima/yashima.h diff --git a/esphome/components/yashima/__init__.py b/esphome/components/yashima/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/yashima/climate.py b/esphome/components/yashima/climate.py new file mode 100644 index 0000000000..5d33670fb5 --- /dev/null +++ b/esphome/components/yashima/climate.py @@ -0,0 +1,36 @@ +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'] + +yashima_ns = cg.esphome_ns.namespace('yashima') +YashimaClimate = yashima_ns.class_('YashimaClimate', climate.Climate, cg.Component) + +CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SUPPORTS_COOL = 'supports_cool' + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(YashimaClimate), + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + 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) + + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + 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/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp new file mode 100644 index 0000000000..e3c0a33127 --- /dev/null +++ b/esphome/components/yashima/yashima.cpp @@ -0,0 +1,193 @@ +#include "yashima.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace yashima { + +static const char *TAG = "yashima.climate"; + +const uint16_t YASHIMA_STATE_LENGTH = 9; +const uint16_t YASHIMA_BITS = YASHIMA_STATE_LENGTH * 8; + +/* the bit masks are intended to be sent from the MSB to the LSB */ +const uint8_t YASHIMA_MODE_HEAT_BYTE0 = 0b00100000; +const uint8_t YASHIMA_MODE_DRY_BYTE0 = 0b01100000; +const uint8_t YASHIMA_MODE_COOL_BYTE0 = 0b11100000; +const uint8_t YASHIMA_MODE_FAN_BYTE0 = 0b10100000; +const uint8_t YASHIMA_MODE_AUTO_BYTE0 = 0b11100000; +const uint8_t YASHIMA_MODE_OFF_BYTE0 = 0b11110000; +const uint8_t YASHIMA_BASE_BYTE0 = 0b1110; + +const uint8_t YASHIMA_TEMP_MAX = 30; // Celsius +const uint8_t YASHIMA_TEMP_MIN = 16; // Celsius +const uint8_t YASHIMA_TEMP_RANGE = YASHIMA_TEMP_MAX - YASHIMA_TEMP_MIN + 1; + +const uint8_t YASHIMA_TEMP_MAP_BYTE1[YASHIMA_TEMP_RANGE] = { + 0b01100100, // 16C + 0b10100100, // 17C + 0b00100100, // 18C + 0b11000100, // 19C + 0b01000100, // 20C + 0b10000100, // 21C + 0b00000100, // 22C + 0b11111000, // 23C + 0b01111000, // 24C + 0b10111000, // 25C + 0b00111000, // 26C + 0b11011000, // 27C + 0b01011000, // 28C + 0b10011000, // 29C + 0b00011000, // 30C +}; +const uint8_t YASHIMA_BASE_BYTE1 = 0b11; + +const uint8_t YASHIMA_FAN_AUTO_BYTE2 = 0b11000000; +const uint8_t YASHIMA_FAN_LOW_BYTE2 = 0b00000000; +const uint8_t YASHIMA_FAN_MEDIUM_BYTE2 = 0b10000000; +const uint8_t YASHIMA_FAN_HIGH_BYTE2 = 0b01000000; +const uint8_t YASHIMA_BASE_BYTE2 = 0b111111; + +const uint8_t YASHIMA_BASE_BYTE3 = 0b11111111; +const uint8_t YASHIMA_BASE_BYTE4 = 0b11; + +const uint8_t YASHIMA_MODE_HEAT_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_DRY_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_FAN_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_AUTO_BYTE5 = 0b00000000; +const uint8_t YASHIMA_MODE_COOL_BYTE5 = 0b10000000; +const uint8_t YASHIMA_MODE_OFF_BYTE5 = 0b10000000; +const uint8_t YASHIMA_BASE_BYTE5 = 0b11111; + +const uint8_t YASHIMA_BASE_BYTE6 = 0b11111111; +const uint8_t YASHIMA_BASE_BYTE7 = 0b11111111; +const uint8_t YASHIMA_BASE_BYTE8 = 0b11001111; + +/* values sampled using a Broadlink Mini 3: */ +// const uint16_t YASHIMA_HEADER_MARK = 9600; +// const uint16_t YASHIMA_HEADER_SPACE = 4800; +// const uint16_t YASHIMA_BIT_MARK = 720; +// const uint16_t YASHIMA_ONE_SPACE = 550; +// const uint16_t YASHIMA_ZERO_SPACE = 1640; + +/* scaled values to get correct timing on ESP8266/ESP32: */ +const uint16_t YASHIMA_HEADER_MARK = 9035; +const uint16_t YASHIMA_HEADER_SPACE = 4517; +const uint16_t YASHIMA_BIT_MARK = 667; +const uint16_t YASHIMA_ONE_SPACE = 517; +const uint16_t YASHIMA_ZERO_SPACE = 1543; +const uint32_t YASHIMA_GAP = YASHIMA_HEADER_SPACE; + +const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000; + +climate::ClimateTraits YashimaClimate::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(YASHIMA_TEMP_MIN); + traits.set_visual_max_temperature(YASHIMA_TEMP_MAX); + traits.set_visual_temperature_step(1); + return traits; +} + +void YashimaClimate::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_OFF; + this->target_temperature = 24; + } +} + +void YashimaClimate::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(); +} + +void YashimaClimate::transmit_state_() { + uint8_t remote_state[YASHIMA_STATE_LENGTH] = {0}; + + remote_state[0] = YASHIMA_BASE_BYTE0; + remote_state[1] = YASHIMA_BASE_BYTE1; + remote_state[2] = YASHIMA_BASE_BYTE2; + remote_state[3] = YASHIMA_BASE_BYTE3; + remote_state[4] = YASHIMA_BASE_BYTE4; + remote_state[5] = YASHIMA_BASE_BYTE5; + remote_state[6] = YASHIMA_BASE_BYTE6; + remote_state[7] = YASHIMA_BASE_BYTE7; + remote_state[8] = YASHIMA_BASE_BYTE8; + + // Set mode + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + remote_state[0] |= YASHIMA_MODE_AUTO_BYTE0; + remote_state[5] |= YASHIMA_MODE_AUTO_BYTE5; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[0] |= YASHIMA_MODE_COOL_BYTE0; + remote_state[5] |= YASHIMA_MODE_COOL_BYTE5; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[0] |= YASHIMA_MODE_HEAT_BYTE0; + remote_state[5] |= YASHIMA_MODE_HEAT_BYTE5; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state[0] |= YASHIMA_MODE_OFF_BYTE0; + remote_state[5] |= YASHIMA_MODE_OFF_BYTE5; + break; + // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY are missing in esphome + } + + // TODO: missing support for fan speed + remote_state[2] |= YASHIMA_FAN_AUTO_BYTE2; + + // Set temperature + uint8_t safecelsius = std::max((uint8_t) this->target_temperature, YASHIMA_TEMP_MIN); + safecelsius = std::min(safecelsius, YASHIMA_TEMP_MAX); + remote_state[1] |= YASHIMA_TEMP_MAP_BYTE1[safecelsius - YASHIMA_TEMP_MIN]; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(YASHIMA_CARRIER_FREQUENCY); + + // Header + data->mark(YASHIMA_HEADER_MARK); + data->space(YASHIMA_HEADER_SPACE); + // Data (sent from the MSB to the LSB) + for (uint8_t i : remote_state) + for (int8_t j = 7; j >= 0; j--) { + data->mark(YASHIMA_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? YASHIMA_ONE_SPACE : YASHIMA_ZERO_SPACE); + } + // Footer + data->mark(YASHIMA_BIT_MARK); + data->space(YASHIMA_GAP); + + transmit.perform(); +} + +} // namespace yashima +} // namespace esphome diff --git a/esphome/components/yashima/yashima.h b/esphome/components/yashima/yashima.h new file mode 100644 index 0000000000..466816bd5f --- /dev/null +++ b/esphome/components/yashima/yashima.h @@ -0,0 +1,40 @@ +#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" + +namespace esphome { +namespace yashima { + +class YashimaClimate : 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; } + + 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}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace yashima +} // namespace esphome