Base code
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import pins
|
||||
from esphome.components import uart
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_HAUSLANE_ID = "hauslane_id"
|
||||
CONF_PIN_TIMER = "pin_timer"
|
||||
CONF_PIN_UP = "pin_up"
|
||||
CONF_PIN_DOWN = "pin_down"
|
||||
CONF_PIN_LIGHT = "pin_light"
|
||||
CONF_PIN_POWER = "pin_power"
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
hauslane_ns = cg.esphome_ns.namespace("hauslane")
|
||||
Hauslane = hauslane_ns.class_(
|
||||
"Hauslane", cg.Component, uart.UARTDevice
|
||||
)
|
||||
HauslaneChild = hauslane_ns.class_("HauslaneChild")
|
||||
|
||||
HAUSLANE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAUSLANE_ID): cv.use_id(Hauslane),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({
|
||||
cv.GenerateID(): cv.declare_id(Hauslane),
|
||||
cv.Optional(CONF_PIN_TIMER): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_PIN_UP): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_PIN_DOWN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_PIN_LIGHT): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_PIN_POWER): pins.gpio_output_pin_schema,
|
||||
})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
if CONF_PIN_TIMER in config:
|
||||
pin_timer = await cg.gpio_pin_expression(config[CONF_PIN_TIMER])
|
||||
cg.add(var.set_pin_timer(pin_timer))
|
||||
if CONF_PIN_UP in config:
|
||||
pin_up = await cg.gpio_pin_expression(config[CONF_PIN_UP])
|
||||
cg.add(var.set_pin_up(pin_up))
|
||||
if CONF_PIN_DOWN in config:
|
||||
pin_down= await cg.gpio_pin_expression(config[CONF_PIN_DOWN])
|
||||
cg.add(var.set_pin_down(pin_down))
|
||||
if CONF_PIN_LIGHT in config:
|
||||
pin_light = await cg.gpio_pin_expression(config[CONF_PIN_LIGHT])
|
||||
cg.add(var.set_pin_light(pin_light))
|
||||
if CONF_PIN_POWER in config:
|
||||
pin_power = await cg.gpio_pin_expression(config[CONF_PIN_POWER])
|
||||
cg.add(var.set_pin_power(pin_power))
|
||||
@@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import fan, output
|
||||
from esphome.const import CONF_OUTPUT_ID
|
||||
from . import (
|
||||
HAUSLANE_SCHEMA,
|
||||
CONF_HAUSLANE_ID,
|
||||
HauslaneChild,
|
||||
hauslane_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["hauslane"]
|
||||
|
||||
HauslaneFan = hauslane_ns.class_(
|
||||
"HauslaneFan", fan.Fan, HauslaneChild
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
fan.FAN_SCHEMA.extend({
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(HauslaneFan),
|
||||
})
|
||||
.extend(HAUSLANE_SCHEMA)
|
||||
)
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await fan.register_fan(var,config)
|
||||
|
||||
paren = await cg.get_variable(config[CONF_HAUSLANE_ID])
|
||||
cg.add(var.set_parent(paren))
|
||||
cg.add(var.setup())
|
||||
@@ -0,0 +1,337 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "hauslane.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace hauslane {
|
||||
|
||||
static const char *TAG = "hauslane";
|
||||
static const int DELAY=250; // how long to press/release buttons in ms
|
||||
|
||||
void Hauslane::setup() {
|
||||
// initialize the input pins
|
||||
if (this->pin_timer) {
|
||||
pin_timer->setup();
|
||||
pin_timer->digital_write(false);
|
||||
}
|
||||
if (this->pin_up) {
|
||||
pin_up->setup();
|
||||
pin_up->digital_write(false);
|
||||
}
|
||||
if (this->pin_down) {
|
||||
pin_down->setup();
|
||||
pin_down->digital_write(false);
|
||||
}
|
||||
if (this->pin_light) {
|
||||
pin_light->setup();
|
||||
pin_light->digital_write(false);
|
||||
}
|
||||
if (this->pin_power) {
|
||||
pin_power->setup();
|
||||
pin_power->digital_write(false);
|
||||
}
|
||||
|
||||
// register ESPHome API service named "command"
|
||||
register_service(&Hauslane::command, "command", {"command"});
|
||||
}
|
||||
|
||||
void Hauslane::loop() {
|
||||
while (this->available()) {
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
|
||||
if (reading==true) {
|
||||
this->rx_message.push_back(c);
|
||||
|
||||
if (c == MSG_END[pos]) {
|
||||
if (pos+1 == END_LEN) {
|
||||
parse_state();
|
||||
reading = false;
|
||||
pos = 0;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
} else {
|
||||
pos = 0;
|
||||
}
|
||||
} else {
|
||||
if (c==MSG_START[pos]) {
|
||||
if (pos+1 == START_LEN) {
|
||||
reading = true;
|
||||
pos = 0;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
} else {
|
||||
pos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Components are required to dump their configuration using ESP_LOGCONFIG in
|
||||
// the dump_config() method. This method is used exclusively to print values
|
||||
// determined during setup() -- nothing more.
|
||||
// dump_config() for parent component
|
||||
void Hauslane::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Hauslane");
|
||||
}
|
||||
|
||||
// turns the light on/off
|
||||
void Hauslane::set_light(bool binary) {
|
||||
if (this->light_cur != binary) {
|
||||
if (speed==0) {
|
||||
// fan is not on, so need to power system on before pressing light button
|
||||
button_press(pin_power);
|
||||
if (binary) {
|
||||
button_press(pin_light);
|
||||
}
|
||||
} else {
|
||||
// fan is on, so only need to press light button
|
||||
button_press(pin_light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the speed of the fan 0=off, 6=full
|
||||
void Hauslane::set_speed(int new_speed) {
|
||||
if (new_speed==0 && !this->light_cur) {
|
||||
// request fan turn off and light is off, so simply hit the power button
|
||||
button_press(pin_power);
|
||||
} else if (this->speed == 0) {
|
||||
// requesting speed from 1-6
|
||||
if (!this->light_cur) {
|
||||
// light is off, so activate hood with power button first
|
||||
button_press(pin_power);
|
||||
}
|
||||
// turn on the fan to previously used speed by pressing down button
|
||||
button_press(pin_down);
|
||||
} else {
|
||||
// adjust speed pressing up/down appropriate number of times
|
||||
while (this->speed != new_speed) {
|
||||
if (this->speed < new_speed) {
|
||||
this->speed++;
|
||||
button_press(pin_up);
|
||||
} else if (this->speed > new_speed) {
|
||||
this->speed--;
|
||||
button_press(pin_down);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read message on rx line and turn into current state of fan and light
|
||||
void Hauslane::parse_state() {
|
||||
size_t len = rx_message.size()-4;
|
||||
std::string msg;
|
||||
std::vector<uint8_t> msg_hex;
|
||||
|
||||
// hex strings for each possible state in the form of X_Y where X=on/off
|
||||
// (light) and Y=0-6 (fan speed)
|
||||
// admittedly this is very ugly - the communication is almost certainly
|
||||
// not UART, but at 3600 baud, the UART decoder is able to read just
|
||||
// enough of the signal as UART characters to get a unique string for
|
||||
// each state, but they seemingly do not follow any patterns (most likely
|
||||
// due to the bits that are discarded by the decoder as not part of a
|
||||
// legitimate UART message) so are hardcoded for each state
|
||||
// proper reverse-engineering of the protocol would require logic analzer or
|
||||
// oscilloscope, but as there is only one-way communication from the front
|
||||
// panel to the main controller board, this would only be useful if
|
||||
// replacing the front panel entirely
|
||||
const std::vector<uint8_t> off_0{0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_0{0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xc6};
|
||||
const std::vector<uint8_t> off_1{0xce, 0xc6, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xc6, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_1{0xce, 0xc6, 0xce, 0xce, 0xce, 0xce, 0xce};
|
||||
const std::vector<uint8_t> off_2{0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xc6, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_2{0xce, 0xc6, 0xce, 0xce, 0xce, 0xce, 0xce, 0xc6, 0xce, 0xc6};
|
||||
const std::vector<uint8_t> off_3{0x38, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_3{0x38, 0xc6, 0xce, 0xce, 0xce, 0xce, 0xce};
|
||||
const std::vector<uint8_t> off_4{0xc6, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xc6, 0xce, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_4{0xc6, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce};
|
||||
const std::vector<uint8_t> off_5{0xc6, 0xc6, 0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0xc6, 0xc6, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_5{0xc6, 0xc6, 0xce, 0xce, 0xce, 0xce, 0xc6};
|
||||
const std::vector<uint8_t> off_6{0xce, 0xce, 0xce, 0xce, 0xce, 0xce, 0x38, 0xce, 0xce};
|
||||
const std::vector<uint8_t> on_6{0xce, 0xc6, 0xce, 0xce, 0xce, 0xce, 0x38};
|
||||
|
||||
// print out formatted hex string to log
|
||||
char buf[5];
|
||||
for (size_t i=0; i< len; i++) {
|
||||
if (i>0) {
|
||||
msg += " ";
|
||||
}
|
||||
msg_hex.push_back(rx_message[i]);
|
||||
sprintf(buf,"%02X", rx_message[i]);
|
||||
msg += buf;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "%s", msg.c_str());
|
||||
|
||||
// set state of light and fan based on received hex message
|
||||
if (msg_hex==off_0) {
|
||||
set_state(false, 0);
|
||||
} else if (msg_hex==on_0) {
|
||||
set_state(true, 0);
|
||||
} else if (msg_hex==off_1) {
|
||||
set_state(false, 1);
|
||||
} else if (msg_hex==on_1) {
|
||||
set_state(true, 1);
|
||||
} else if (msg_hex==off_2) {
|
||||
set_state(false, 2);
|
||||
} else if (msg_hex==on_2) {
|
||||
set_state(true, 2);
|
||||
} else if (msg_hex==off_3) {
|
||||
set_state(false, 3);
|
||||
} else if (msg_hex==on_3) {
|
||||
set_state(true, 3);
|
||||
} else if (msg_hex==off_4) {
|
||||
set_state(false, 4);
|
||||
} else if (msg_hex==on_4) {
|
||||
set_state(true, 4);
|
||||
} else if (msg_hex==off_5) {
|
||||
set_state(false, 5);
|
||||
} else if (msg_hex==on_5) {
|
||||
set_state(true, 5);
|
||||
} else if (msg_hex==off_6) {
|
||||
set_state(false, 6);
|
||||
} else if (msg_hex==on_6) {
|
||||
set_state(true, 6);
|
||||
}
|
||||
this->rx_message.clear();
|
||||
}
|
||||
|
||||
void Hauslane::set_state(bool set_light, uint8_t set_speed) {
|
||||
if (this->light_cur != set_light) {
|
||||
// save current state of light in memory
|
||||
ESP_LOGD(TAG, "Received light state: %d", set_light);
|
||||
this->light_cur = set_light;
|
||||
// send light state to API if it is active
|
||||
if (this->send_light_state) {
|
||||
ESP_LOGD(TAG, "Sending new light state.");
|
||||
this->send_light_state(set_light);
|
||||
}
|
||||
}
|
||||
if (speed != set_speed) {
|
||||
// save current state of fan in memory
|
||||
ESP_LOGD(TAG, "Received fan peed: %d", set_speed);
|
||||
this->speed = set_speed;
|
||||
// send fan state to API if it is active
|
||||
if (this->send_fan_speed) {
|
||||
ESP_LOGD(TAG,"Sending new fan speed.");
|
||||
this->send_fan_speed(set_speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// custom component API commands
|
||||
// simulates a button press for the specified button on front panel via GPIO
|
||||
void Hauslane::command(std::string command) {
|
||||
if (command == "timer" && pin_timer) {
|
||||
button_press(pin_timer);
|
||||
} else if (command == "up" && pin_up) {
|
||||
button_press(pin_up);
|
||||
} else if (command =="down" && pin_down) {
|
||||
button_press(pin_down);
|
||||
} else if (command == "light" && pin_light) {
|
||||
button_press(pin_light);
|
||||
} else if (command == "power" && pin_power) {
|
||||
button_press(pin_power);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Invalid button name: %s", command.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// simulate a button press by activating the GPIO then releasing
|
||||
void Hauslane::button_press(GPIOPin *pin) {
|
||||
ESP_LOGD(TAG, "Press button: %d", pin);
|
||||
pin->digital_write(true);
|
||||
delay(DELAY);
|
||||
pin->digital_write(false);
|
||||
delay(DELAY);
|
||||
}
|
||||
|
||||
// ESPHome light component
|
||||
#ifdef USE_LIGHT
|
||||
void HauslaneLight::setup() {
|
||||
this->parent->register_light_func(
|
||||
[this](const bool &binary) {
|
||||
this->set_state(binary);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Components are required to dump their configuration using ESP_LOGCONFIG in
|
||||
// the dump_config() method. This method is used exclusively to print values
|
||||
// determined during setup() -- nothing more.
|
||||
// dump_config() for light component
|
||||
void HauslaneLight::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Hauslane");
|
||||
}
|
||||
|
||||
// set to light that can turn on/off only
|
||||
light::LightTraits HauslaneLight::get_traits() {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supported_color_modes({light::ColorMode::ON_OFF});
|
||||
return traits;
|
||||
}
|
||||
|
||||
// when light tries to write state, use Hauslane::set_light
|
||||
void HauslaneLight::write_state(light::LightState *state) {
|
||||
bool binary;
|
||||
state->current_values_as_binary(&binary);
|
||||
this->parent->set_light(binary);
|
||||
}
|
||||
|
||||
void HauslaneLight::set_state(bool binary) {
|
||||
auto call = light_state_->make_call();
|
||||
call.set_state(binary);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
|
||||
// ESPHome fan component
|
||||
#ifdef USE_FAN
|
||||
void HauslaneFan::setup() {
|
||||
this->parent->register_fan_func(
|
||||
[this](const int &speed) {
|
||||
this->set_speed(speed);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// set to fan with no oscillation, speed enabled, no direction, and 6 speeds
|
||||
fan::FanTraits HauslaneFan::get_traits() {
|
||||
return fan::FanTraits(false, true, false, 6);
|
||||
}
|
||||
|
||||
// when fan tries to change state, use Hauslane::set_speed
|
||||
void HauslaneFan::control(const fan::FanCall &call){
|
||||
if (*call.get_state()) {
|
||||
if (call.get_speed().has_value()) {
|
||||
this->parent->set_speed(*call.get_speed());
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Power fan off by setting speed to 0");
|
||||
this->parent->set_speed(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Components are required to dump their configuration using ESP_LOGCONFIG in
|
||||
// the dump_config() method. This method is used exclusively to print values
|
||||
// determined during setup() -- nothing more.
|
||||
// dump_config() for fan component
|
||||
void HauslaneFan::dump_config(){
|
||||
ESP_LOGCONFIG(TAG, "Hauslane");
|
||||
}
|
||||
|
||||
void HauslaneFan::set_speed(int new_speed) {
|
||||
if (new_speed>0) {
|
||||
this->state = true;
|
||||
} else {
|
||||
this->state = false;
|
||||
}
|
||||
this->speed=new_speed;
|
||||
this->publish_state();
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/api/custom_api_device.h"
|
||||
#ifdef USE_LIGHT
|
||||
#include "esphome/components/light/light_output.h"
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
#include "esphome/components/fan/fan.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace hauslane {
|
||||
|
||||
static const unsigned char MSG_START[] = {0x08, 0x08, 0x08, 0xfe, 0xc0};
|
||||
static const unsigned char MSG_END[] = {0xc0, 0xce, 0xce, 0xce};
|
||||
static const uint8_t START_LEN=5;
|
||||
static const uint8_t END_LEN=4;
|
||||
|
||||
class Hauslane : public Component, public uart::UARTDevice, public api::CustomAPIDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void set_pin_timer(GPIOPin *pin) {this->pin_timer=pin;}
|
||||
void set_pin_up(GPIOPin *pin) {this->pin_up=pin;}
|
||||
void set_pin_down(GPIOPin *pin) {this->pin_down=pin;}
|
||||
void set_pin_light(GPIOPin *pin) {this->pin_light=pin;}
|
||||
void set_pin_power(GPIOPin *pin) {this->pin_power=pin;}
|
||||
void set_light(bool binary);
|
||||
void set_speed(int new_speed);
|
||||
void register_light_func(const std::function<void(bool)> &func) { this->send_light_state = func; }
|
||||
void register_fan_func(const std::function<void(int)> &func) { this->send_fan_speed = func; }
|
||||
|
||||
protected:
|
||||
bool reading = false;
|
||||
size_t pos = 0;
|
||||
std::vector<uint8_t> rx_message;
|
||||
void parse_state();
|
||||
void set_state(bool set_light, uint8_t set_speed);
|
||||
bool light_cur=false;
|
||||
uint8_t speed=0;
|
||||
GPIOPin *pin_timer;
|
||||
GPIOPin *pin_up;
|
||||
GPIOPin *pin_down;
|
||||
GPIOPin *pin_light;
|
||||
GPIOPin *pin_power;
|
||||
void command(std::string command);
|
||||
void button_press(GPIOPin *pin);
|
||||
std::function<void(bool)> send_light_state;
|
||||
std::function<void(int)> send_fan_speed;
|
||||
};
|
||||
|
||||
class HauslaneChild {
|
||||
public:
|
||||
void set_parent(Hauslane *parent) {this->parent = parent;}
|
||||
|
||||
protected:
|
||||
Hauslane *parent;
|
||||
};
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
class HauslaneLight : public light::LightOutput, public Component, public HauslaneChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
light::LightTraits get_traits() override;
|
||||
void setup_state(light::LightState *state) override {this->light_state_ = state;}
|
||||
void set_state(bool binary);
|
||||
|
||||
protected:
|
||||
void write_state(light::LightState *state) override;
|
||||
light::LightState *light_state_{nullptr};
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
class HauslaneFan : public fan::Fan, public Component, public HauslaneChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
fan::FanTraits get_traits() override;
|
||||
void set_speed(int speed);
|
||||
|
||||
protected:
|
||||
void control(const fan::FanCall &call) override;
|
||||
};
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import light
|
||||
from esphome.const import CONF_OUTPUT_ID
|
||||
from . import (
|
||||
HAUSLANE_SCHEMA,
|
||||
CONF_HAUSLANE_ID,
|
||||
HauslaneChild,
|
||||
hauslane_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["hauslane"]
|
||||
|
||||
HauslaneLight = hauslane_ns.class_(
|
||||
"HauslaneLight", light.LightOutput, HauslaneChild
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
light.LIGHT_SCHEMA.extend({
|
||||
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(HauslaneLight),
|
||||
})
|
||||
.extend(HAUSLANE_SCHEMA)
|
||||
)
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await light.register_light(var,config)
|
||||
|
||||
paren = await cg.get_variable(config[CONF_HAUSLANE_ID])
|
||||
cg.add(var.set_parent(paren))
|
||||
cg.add(var.setup())
|
||||
Reference in New Issue
Block a user