Move heater implementation to canonical component
This commit is contained in:
+23
-95
@@ -2,118 +2,46 @@ esphome:
|
|||||||
name: bathroomheater
|
name: bathroomheater
|
||||||
platform: ESP8266
|
platform: ESP8266
|
||||||
board: nodemcuv2
|
board: nodemcuv2
|
||||||
|
includes:
|
||||||
|
- zehnder.h
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
|
|
||||||
|
web_server:
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
broker: mqtt.lan
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin:
|
pin:
|
||||||
number: D4
|
number: D4
|
||||||
inverted: true
|
inverted: true
|
||||||
restore_mode: ALWAYS_OFF
|
restore_mode: ALWAYS_OFF
|
||||||
name: "Built-in LED"
|
name: "Bathroom Heater LED"
|
||||||
|
|
||||||
web_server:
|
|
||||||
port: 80
|
|
||||||
|
|
||||||
remote_transmitter:
|
remote_transmitter:
|
||||||
pin: D2
|
pin: D2
|
||||||
carrier_duty_percent: 50%
|
carrier_duty_percent: 50%
|
||||||
|
id: heater_ir_transmitter
|
||||||
|
|
||||||
globals:
|
climate:
|
||||||
- id: heater_code
|
- platform: custom
|
||||||
type: int[38]
|
lambda: |-
|
||||||
initial_value: |
|
auto heater = new Zehnder();
|
||||||
{
|
App.register_component(heater);
|
||||||
// header
|
heater->set_transmitter(id(heater_ir_transmitter));
|
||||||
30, -1000,
|
return {heater};
|
||||||
// 0xB8 (0b10111000)
|
climates:
|
||||||
30, -830,
|
- name: "Bathroom Heater"
|
||||||
30, -650,
|
id: bathroom_heater
|
||||||
30, -830,
|
|
||||||
30, -830,
|
|
||||||
30, -830,
|
|
||||||
30, -650,
|
|
||||||
30, -650,
|
|
||||||
30, -650,
|
|
||||||
// 0x0D (0b00001101)
|
|
||||||
30, -650,
|
|
||||||
30, -650,
|
|
||||||
30, -650,
|
|
||||||
30, -650,
|
|
||||||
30, -830,
|
|
||||||
30, -830,
|
|
||||||
30, -650,
|
|
||||||
30, -830,
|
|
||||||
// post
|
|
||||||
30, -460,
|
|
||||||
// zero
|
|
||||||
30, -650
|
|
||||||
}
|
|
||||||
- id: heater_level
|
|
||||||
type: int
|
|
||||||
initial_value: "0"
|
|
||||||
- id: heater_level_code_map
|
|
||||||
type: int[9]
|
|
||||||
initial_value: |
|
|
||||||
{
|
|
||||||
0x0D, // OFF
|
|
||||||
0x86,
|
|
||||||
0x95,
|
|
||||||
0xA0,
|
|
||||||
0xB3,
|
|
||||||
0xCA,
|
|
||||||
0xD9,
|
|
||||||
0xEC,
|
|
||||||
0xFF
|
|
||||||
}
|
|
||||||
|
|
||||||
script:
|
|
||||||
- id: transmit_heater_code
|
|
||||||
then:
|
|
||||||
# https://esphome.io/components/climate/custom.html
|
|
||||||
- remote_transmitter.transmit_raw:
|
|
||||||
carrier_frequency: 455000
|
|
||||||
repeat:
|
|
||||||
times: 10
|
|
||||||
wait_time: 10ms
|
|
||||||
code: !lambda |-
|
|
||||||
if (id(heater_level) > 8 || id(heater_level) < 0) {
|
|
||||||
id(heater_level) = 8;
|
|
||||||
}
|
|
||||||
int level_code = id(heater_level_code_map)[id(heater_level)];
|
|
||||||
for (int i = 0; i < 8; ++i) {
|
|
||||||
id(heater_code)[i*2 + 18] = 30;
|
|
||||||
id(heater_code)[i*2 + 19] =
|
|
||||||
level_code & 0b10000000 ? -830 : -650;
|
|
||||||
level_code <<= 1;
|
|
||||||
}
|
|
||||||
std::vector<int> codes = std::vector<int>(
|
|
||||||
id(heater_code),
|
|
||||||
id(heater_code) + 38);
|
|
||||||
for (int i = 0; i < codes.size(); ++i) {
|
|
||||||
ESP_LOGD("code", "%d: %d", i, codes[i]);
|
|
||||||
}
|
|
||||||
return codes;
|
|
||||||
- mqtt.publish:
|
|
||||||
topic: bathroomheater/heater/state
|
|
||||||
payload: !lambda "return to_string(id(heater_level));"
|
|
||||||
|
|
||||||
mqtt:
|
|
||||||
broker: mqtt.lan
|
|
||||||
on_json_message:
|
|
||||||
topic: bathroomheater/heater/command
|
|
||||||
then:
|
|
||||||
- lambda: |-
|
|
||||||
if (x.containsKey("level")) {
|
|
||||||
id(heater_level) = x["level"];
|
|
||||||
}
|
|
||||||
- script.execute: transmit_heater_code
|
|
||||||
|
|
||||||
interval:
|
interval:
|
||||||
- interval: 1min
|
- interval: 1min
|
||||||
then:
|
then:
|
||||||
- script.execute: transmit_heater_code
|
# Periodically remind heater about the current setting.
|
||||||
|
lambda: static_cast<Zehnder*>(id(bathroom_heater))->retransmit();
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
password: ""
|
password: ""
|
||||||
@@ -124,7 +52,7 @@ wifi:
|
|||||||
|
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
ssid: "Bathroomheater Fallback Hotspot"
|
ssid: "bathroomheater"
|
||||||
password: !secret wifi_fallback_hotspot_password
|
password: !secret wifi_fallback_hotspot_password
|
||||||
|
|
||||||
captive_portal:
|
captive_portal:
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#include "esphome.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
static const char* const TAG = "zehnder";
|
||||||
|
static const uint8_t MIN_TEMPERATURE = 35;
|
||||||
|
static const uint8_t MAX_TEMPERATURE = 70;
|
||||||
|
static const uint8_t TEMPERATURE_LEVELS = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Zehnder : public Component, public Climate {
|
||||||
|
public:
|
||||||
|
void retransmit() {
|
||||||
|
this->transmit_temperature_(this->target_temperature);
|
||||||
|
}
|
||||||
|
|
||||||
|
void control(const ClimateCall &call) override {
|
||||||
|
if (call.get_mode().has_value()) {
|
||||||
|
ClimateMode mode = *call.get_mode();
|
||||||
|
if (mode == climate::CLIMATE_MODE_OFF) {
|
||||||
|
if (this->transmit_level_(0)) {
|
||||||
|
this->target_temperature = 0;
|
||||||
|
this->mode = mode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this->transmit_temperature_(MAX_TEMPERATURE)) {
|
||||||
|
this->target_temperature = MAX_TEMPERATURE;
|
||||||
|
this->mode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (call.get_target_temperature().has_value()) {
|
||||||
|
float temp = *call.get_target_temperature();
|
||||||
|
if (this->transmit_temperature_(temp)) {
|
||||||
|
this->target_temperature = temp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->publish_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClimateTraits traits() override {
|
||||||
|
auto traits = climate::ClimateTraits();
|
||||||
|
|
||||||
|
traits.set_visual_min_temperature(MIN_TEMPERATURE);
|
||||||
|
traits.set_visual_max_temperature(MAX_TEMPERATURE);
|
||||||
|
traits.set_visual_temperature_step(
|
||||||
|
(MAX_TEMPERATURE - MIN_TEMPERATURE) / (TEMPERATURE_LEVELS - 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
traits.set_supported_modes({
|
||||||
|
climate::CLIMATE_MODE_HEAT,
|
||||||
|
climate::CLIMATE_MODE_OFF
|
||||||
|
});
|
||||||
|
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
|
||||||
|
this->transmitter_ = transmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
remote_transmitter::RemoteTransmitterComponent *transmitter_;
|
||||||
|
|
||||||
|
bool transmit_temperature_(float temp) {
|
||||||
|
return this->transmit_level_(temp == 0
|
||||||
|
? 0
|
||||||
|
: (
|
||||||
|
(temp - MIN_TEMPERATURE) *
|
||||||
|
(TEMPERATURE_LEVELS - 1) /
|
||||||
|
(MAX_TEMPERATURE - MIN_TEMPERATURE) +
|
||||||
|
1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool transmit_level_(uint8_t level) {
|
||||||
|
if (level > 8) {
|
||||||
|
ESP_LOGE(TAG, "Unsupported heater level: %d. Ignoring command", level);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this->transmitter_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "IR transmitter not set. Use set_transmitter.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Transmitting heater level: %d...", level);
|
||||||
|
|
||||||
|
this->transmitter_->set_carrier_duty_percent(50);
|
||||||
|
auto call = this->transmitter_->transmit();
|
||||||
|
call.set_send_times(10);
|
||||||
|
call.set_send_wait(10); // milliseconds
|
||||||
|
auto data = call.get_data();
|
||||||
|
|
||||||
|
data->reset();
|
||||||
|
data->set_carrier_frequency(455000);
|
||||||
|
|
||||||
|
static uint8_t mode_bytes[TEMPERATURE_LEVELS + 1] = {
|
||||||
|
0x0D, // OFF
|
||||||
|
0x86,
|
||||||
|
0x95,
|
||||||
|
0xA0,
|
||||||
|
0xB3,
|
||||||
|
0xCA,
|
||||||
|
0xD9,
|
||||||
|
0xEC,
|
||||||
|
0xFF
|
||||||
|
};
|
||||||
|
|
||||||
|
add_header_(data);
|
||||||
|
add_byte_(data, 0xB8);
|
||||||
|
add_byte_(data, mode_bytes[level]);
|
||||||
|
add_post_(data);
|
||||||
|
|
||||||
|
call.perform();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_header_(remote_base::RemoteTransmitData *data) {
|
||||||
|
data->item(30, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_byte_(remote_base::RemoteTransmitData *data, uint8_t value) {
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
data->item(30, value & 0b10000000 ? 830 : 650);
|
||||||
|
value <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void add_post_(remote_base::RemoteTransmitData *data) {
|
||||||
|
data->item(30, 460);
|
||||||
|
data->item(30, 650);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user