├── README.md ├── components ├── zehnder │ ├── __init__.py │ ├── fan.py │ ├── zehnder.h │ └── zehnder.cpp └── nrf905 │ ├── __init__.py │ ├── nRF905.h │ └── nRF905.cpp ├── .gitignore └── utility-bridge.yaml /README.md: -------------------------------------------------------------------------------- 1 | # ESPHome-Zehnder-RF -------------------------------------------------------------------------------- /components/zehnder/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | /secrets.yaml 6 | 7 | .build 8 | 9 | **/__pycache__ 10 | -------------------------------------------------------------------------------- /components/zehnder/fan.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import fan 4 | from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL 5 | 6 | from esphome.components.nrf905 import nRF905Component 7 | 8 | 9 | DEPENDENCIES = ["nrf905"] 10 | 11 | zehnder_ns = cg.esphome_ns.namespace("zehnder") 12 | ZehnderRF = zehnder_ns.class_("ZehnderRF", fan.FanState) 13 | 14 | CONF_NRF905 = "nrf905" 15 | 16 | CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( 17 | { 18 | cv.GenerateID(): cv.declare_id(ZehnderRF), 19 | cv.Required(CONF_NRF905): cv.use_id(nRF905Component), 20 | cv.Optional(CONF_UPDATE_INTERVAL, default="30s"): cv.update_interval, 21 | } 22 | ).extend(cv.COMPONENT_SCHEMA) 23 | 24 | 25 | async def to_code(config): 26 | var = cg.new_Pvariable(config[CONF_ID]) 27 | await cg.register_component(var, config) 28 | await fan.register_fan(var, config) 29 | 30 | nrf905 = await cg.get_variable(config[CONF_NRF905]) 31 | cg.add(var.set_rf(nrf905)) 32 | 33 | cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) 34 | -------------------------------------------------------------------------------- /components/nrf905/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome import pins 4 | from esphome.components import fan, spi 5 | from esphome.const import CONF_ID 6 | 7 | CONF_AM_PIN = "am_pin" 8 | CONF_CD_PIN = "cd_pin" 9 | CONF_CE_PIN = "ce_pin" 10 | CONF_DR_PIN = "dr_pin" 11 | CONF_PWR_PIN = "pwr_pin" 12 | CONF_TXEN_PIN = "txen_pin" 13 | 14 | DEPENDENCIES = ["spi"] 15 | 16 | nrf905_ns = cg.esphome_ns.namespace("nrf905") 17 | nRF905Component = nrf905_ns.class_("nRF905", fan.FanState) 18 | 19 | CONFIG_SCHEMA = ( 20 | cv.Schema( 21 | { 22 | cv.GenerateID(): cv.declare_id(nRF905Component), 23 | cv.Optional(CONF_CD_PIN): pins.gpio_input_pin_schema, 24 | cv.Required(CONF_CE_PIN): pins.gpio_output_pin_schema, 25 | cv.Required(CONF_PWR_PIN): pins.gpio_output_pin_schema, 26 | cv.Required(CONF_TXEN_PIN): pins.gpio_output_pin_schema, 27 | cv.Optional(CONF_AM_PIN): pins.gpio_input_pin_schema, 28 | cv.Optional(CONF_DR_PIN): pins.gpio_input_pin_schema, 29 | } 30 | ) 31 | .extend(cv.COMPONENT_SCHEMA) 32 | .extend(spi.spi_device_schema(cs_pin_required=True)) 33 | ) 34 | 35 | 36 | async def to_code(config): 37 | var = cg.new_Pvariable(config[CONF_ID]) 38 | await cg.register_component(var, config) 39 | await spi.register_spi_device(var, config) 40 | 41 | if CONF_AM_PIN in config: 42 | data = await cg.gpio_pin_expression(config[CONF_AM_PIN]) 43 | cg.add(var.set_am_pin(data)) 44 | if CONF_CD_PIN in config: 45 | data = await cg.gpio_pin_expression(config[CONF_CD_PIN]) 46 | cg.add(var.set_cd_pin(data)) 47 | data = await cg.gpio_pin_expression(config[CONF_CE_PIN]) 48 | cg.add(var.set_ce_pin(data)) 49 | if CONF_DR_PIN in config: 50 | data = await cg.gpio_pin_expression(config[CONF_DR_PIN]) 51 | cg.add(var.set_dr_pin(data)) 52 | data = await cg.gpio_pin_expression(config[CONF_PWR_PIN]) 53 | cg.add(var.set_pwr_pin(data)) 54 | data = await cg.gpio_pin_expression(config[CONF_TXEN_PIN]) 55 | cg.add(var.set_txen_pin(data)) 56 | -------------------------------------------------------------------------------- /utility-bridge.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | devicename: utility_bridge 3 | upper_devicename: Utility Bridge 4 | 5 | esphome: 6 | name: utility-bridge 7 | comment: Fan controller 8 | 9 | esp32: 10 | board: esp32doit-devkit-v1 11 | framework: 12 | type: arduino 13 | 14 | # Enable logging 15 | logger: 16 | level: DEBUG 17 | 18 | # Enable Home Assistant API 19 | api: 20 | password: !secret esphome_utility_bridge_api_password 21 | 22 | services: 23 | - service: set_speed 24 | variables: 25 | run_speed: int 26 | run_time: int 27 | then: 28 | - lambda: |- 29 | zehnder_fan->setSpeed(run_speed, run_time); 30 | 31 | ota: 32 | password: !secret esphome_utility_bridge_ota_password 33 | 34 | wifi: 35 | ssid: !secret wifi_ssid 36 | password: !secret wifi_password 37 | 38 | manual_ip: 39 | static_ip: !secret wifi_ip_utility_bridge 40 | gateway: !secret wifi_gateway 41 | subnet: !secret wifi_subnet 42 | dns1: !secret wifi_gateway 43 | 44 | # Enable fallback hotspot (captive portal) in case wifi connection fails 45 | ap: 46 | ssid: "utility-bridge Fallback" 47 | password: !secret esphome_utility_bridge_ap_password 48 | 49 | captive_portal: 50 | 51 | time: 52 | - platform: homeassistant 53 | id: homeassistant_time 54 | timezone: Europe/Amsterdam 55 | 56 | button: 57 | - platform: restart 58 | id: ${devicename}_esphome_restart 59 | name: ${upper_devicename} herstart 60 | 61 | - platform: template 62 | id: ${devicename}_high_10 63 | name: ${upper_devicename} High 10 64 | on_press: 65 | then: 66 | lambda: |- 67 | zehnder_fan->setSpeed(3, 10); 68 | 69 | - platform: template 70 | id: ${devicename}_high_30 71 | name: ${upper_devicename} High 30 72 | on_press: 73 | then: 74 | lambda: |- 75 | zehnder_fan->setSpeed(3, 30); 76 | 77 | # Load external components 78 | # https://esphome.io/components/external_components.html#external-components-git 79 | external_components: 80 | - source: github://Sanderhuisman/ESPHome-Zehnder-RF 81 | 82 | # SPI 83 | spi: 84 | clk_pin: GPIO14 85 | mosi_pin: GPIO13 86 | miso_pin: GPIO12 87 | 88 | # nRF905 config 89 | nrf905: 90 | id: "nrf905_rf" 91 | cs_pin: GPIO23 92 | cd_pin: GPIO33 93 | ce_pin: GPIO27 94 | pwr_pin: GPIO26 95 | txen_pin: GPIO25 96 | # We don't need AM and DR at the moment as they are read from the inernal registers 97 | # am_pin: GPIO32 98 | # dr_pin: GPIO35 99 | 100 | # The FAN controller 101 | fan: 102 | - platform: zehnder 103 | id: zehnder_fan 104 | name: "Ventilation" 105 | nrf905: nrf905_rf 106 | update_interval: "60s" 107 | -------------------------------------------------------------------------------- /components/zehnder/zehnder.h: -------------------------------------------------------------------------------- 1 | #ifndef __COMPONENT_ZEHNDER_H__ 2 | #define __COMPONENT_ZEHNDER_H__ 3 | 4 | #include "esphome/core/component.h" 5 | #include "esphome/core/hal.h" 6 | #include "esphome/components/spi/spi.h" 7 | #include "esphome/components/fan/fan_state.h" 8 | #include "esphome/components/nrf905/nRF905.h" 9 | 10 | namespace esphome { 11 | namespace zehnder { 12 | 13 | #define FAN_FRAMESIZE 16 // Each frame consists of 16 bytes 14 | #define FAN_TX_FRAMES 4 // Retransmit every transmitted frame 4 times 15 | #define FAN_TX_RETRIES 10 // Retry transmission 10 times if no reply is received 16 | #define FAN_TTL 250 // 0xFA, default time-to-live for a frame 17 | #define FAN_REPLY_TIMEOUT 1000 // Wait 500ms for receiving a reply when doing a network scan 18 | 19 | /* Fan device types */ 20 | enum { 21 | FAN_TYPE_BROADCAST = 0x00, // Broadcast to all devices 22 | FAN_TYPE_MAIN_UNIT = 0x01, // Fans 23 | FAN_TYPE_REMOTE_CONTROL = 0x03, // Remote controls 24 | FAN_TYPE_CO2_SENSOR = 0x18 25 | }; // CO2 sensors 26 | 27 | /* Fan commands */ 28 | enum { 29 | FAN_FRAME_SETVOLTAGE = 0x01, // Set speed (voltage / percentage) 30 | FAN_FRAME_SETSPEED = 0x02, // Set speed (preset) 31 | FAN_FRAME_SETTIMER = 0x03, // Set speed with timer 32 | FAN_NETWORK_JOIN_REQUEST = 0x04, 33 | FAN_FRAME_SETSPEED_REPLY = 0x05, 34 | FAN_NETWORK_JOIN_OPEN = 0x06, 35 | FAN_TYPE_FAN_SETTINGS = 0x07, // Current settings, sent by fan in reply to 0x01, 0x02, 0x10 36 | FAN_FRAME_0B = 0x0B, 37 | FAN_NETWORK_JOIN_ACK = 0x0C, 38 | // FAN_NETWORK_JOIN_FINISH = 0x0D, 39 | FAN_TYPE_QUERY_NETWORK = 0x0D, 40 | FAN_TYPE_QUERY_DEVICE = 0x10, 41 | FAN_FRAME_SETVOLTAGE_REPLY = 0x1D 42 | }; 43 | 44 | /* Fan speed presets */ 45 | enum { 46 | FAN_SPEED_AUTO = 0x00, // Off: 0% or 0.0 volt 47 | FAN_SPEED_LOW = 0x01, // Low: 30% or 3.0 volt 48 | FAN_SPEED_MEDIUM = 0x02, // Medium: 50% or 5.0 volt 49 | FAN_SPEED_HIGH = 0x03, // High: 90% or 9.0 volt 50 | FAN_SPEED_MAX = 0x04 51 | }; // Max: 100% or 10.0 volt 52 | 53 | #define NETWORK_LINK_ID 0xA55A5AA5 54 | #define NETWORK_DEFAULT_ID 0xE7E7E7E7 55 | #define FAN_JOIN_DEFAULT_TIMEOUT 10000 56 | 57 | typedef enum { ResultOk, ResultBusy, ResultFailure } Result; 58 | 59 | class ZehnderRF : public Component, public fan::Fan { 60 | public: 61 | ZehnderRF(); 62 | 63 | void setup() override; 64 | 65 | // Setup things 66 | void set_rf(nrf905::nRF905 *const pRf) { rf_ = pRf; } 67 | 68 | void set_update_interval(const uint32_t interval) { interval_ = interval; } 69 | 70 | void dump_config() override; 71 | 72 | fan::FanTraits get_traits() override; 73 | int get_speed_count() { return this->speed_count_; } 74 | 75 | void loop() override; 76 | 77 | void control(const fan::FanCall &call) override; 78 | 79 | float get_setup_priority() const override { return setup_priority::DATA; } 80 | 81 | void setSpeed(const uint8_t speed, const uint8_t timer = 0); 82 | 83 | protected: 84 | void queryDevice(void); 85 | 86 | uint8_t createDeviceID(void); 87 | void discoveryStart(const uint8_t deviceId); 88 | 89 | Result startTransmit(const uint8_t *const pData, const int8_t rxRetries = -1, 90 | const std::function callback = NULL); 91 | void rfComplete(void); 92 | void rfHandler(void); 93 | void rfHandleReceived(const uint8_t *const pData, const uint8_t dataLength); 94 | 95 | typedef enum { 96 | StateStartup, 97 | StateStartDiscovery, 98 | StateDiscoveryWaitForLinkRequest, 99 | StateDiscoveryWaitForJoinResponse, 100 | StateDiscoveryJoinComplete, 101 | 102 | StateIdle, 103 | StateWaitQueryResponse, 104 | StateWaitSetSpeedResponse, 105 | StateWaitSetSpeedConfirm, 106 | 107 | StateNrOf // Keep last 108 | } State; 109 | State state_{StateStartup}; 110 | int speed_count_{}; 111 | 112 | nrf905::nRF905 *rf_; 113 | uint32_t interval_; 114 | 115 | uint8_t _txFrame[FAN_FRAMESIZE]; 116 | 117 | ESPPreferenceObject pref_; 118 | 119 | typedef struct { 120 | uint32_t fan_networkId; // Fan (Zehnder/BUVA) network ID 121 | uint8_t fan_my_device_type; // Fan (Zehnder/BUVA) device type 122 | uint8_t fan_my_device_id; // Fan (Zehnder/BUVA) device ID 123 | uint8_t fan_main_unit_type; // Fan (Zehnder/BUVA) main unit type 124 | uint8_t fan_main_unit_id; // Fan (Zehnder/BUVA) main unit ID 125 | } Config; 126 | Config config_; 127 | 128 | uint32_t lastFanQuery_{0}; 129 | std::function onReceiveTimeout_ = NULL; 130 | 131 | uint32_t msgSendTime_{0}; 132 | uint32_t airwayFreeWaitTime_{0}; 133 | int8_t retries_{-1}; 134 | 135 | uint8_t newSpeed{0}; 136 | uint8_t newTimer{0}; 137 | bool newSetting{false}; 138 | 139 | typedef enum { 140 | RfStateIdle, // Idle state 141 | RfStateWaitAirwayFree, // wait for airway free 142 | RfStateTxBusy, // 143 | RfStateRxWait, 144 | } RfState; 145 | RfState rfState_{RfStateIdle}; 146 | }; 147 | 148 | } // namespace zehnder 149 | } // namespace esphome 150 | 151 | #endif /* __COMPONENT_ZEHNDER_H__ */ 152 | -------------------------------------------------------------------------------- /components/nrf905/nRF905.h: -------------------------------------------------------------------------------- 1 | #ifndef __COMPONENT_nRF905_H__ 2 | #define __COMPONENT_nRF905_H__ 3 | 4 | #include "esphome/core/component.h" 5 | #include "esphome/core/hal.h" 6 | #include "esphome/core/helpers.h" 7 | #include "esphome/components/spi/spi.h" 8 | #include "nRF905.h" 9 | 10 | namespace esphome { 11 | namespace nrf905 { 12 | 13 | #define MAX_TRANSMIT_TIME 2000 // TODO figure out what timeout we want 14 | #define CARRIERDETECT_LED_DELAY 20 // On-board LED will light up for 20ms when data is received 15 | 16 | /* nRF905 register sizes */ 17 | #define NRF905_REGISTER_COUNT 10 18 | #define NRF905_MAX_FRAMESIZE 32 19 | 20 | /* nRF905 Instructions */ 21 | #define NRF905_COMMAND_NOP 0xFF 22 | #define NRF905_COMMAND_W_CONFIG 0x00 23 | #define NRF905_COMMAND_R_CONFIG 0x10 24 | #define NRF905_COMMAND_W_TX_PAYLOAD 0x20 25 | #define NRF905_COMMAND_R_TX_PAYLOAD 0x21 26 | #define NRF905_COMMAND_W_TX_ADDRESS 0x22 27 | #define NRF905_COMMAND_R_TX_ADDRESS 0x23 28 | #define NRF905_COMMAND_R_RX_PAYLOAD 0x24 29 | #define NRF905_COMMAND_CHANNEL_CONFIG 0x80 30 | 31 | // Bit positions 32 | #define NRF905_STATUS_DR 5 33 | #define NRF905_STATUS_AM 7 34 | 35 | typedef enum { 36 | Ok, 37 | Failure, 38 | } nRF905Cc; 39 | 40 | typedef enum { PowerDown, Idle, Receive, Transmit } Mode; 41 | 42 | typedef enum { 43 | ClkOut4000000 = 0x00, 44 | ClkOut2000000 = 0x01, 45 | ClkOut1000000 = 0x02, 46 | ClkOut500000 = 0x03, 47 | } ClkOut; 48 | 49 | typedef enum { PowerNormal = 0x00, PowerReduced = 0x01 } RxPower; 50 | 51 | typedef struct { 52 | uint16_t channel; // nRF905 RF channel 53 | bool band; // nRF905 href_ppl: false=434MHz band, true=868MHZ band 54 | RxPower rx_power; // nRF905 Receive power: false=normal, true=reduced 55 | bool auto_retransmit; // nRF905 Auto retransmission flag: false=off, true=on 56 | uint32_t rx_address; // nRF905 Receive address 57 | uint8_t rx_address_width; // nRF905 Receive address size (1-4 bytes) 58 | uint8_t rx_payload_width; // nRF905 Receive payload size (1-32 bytes) 59 | // uint32_t tx_address; // nRF905 Transmit address 60 | uint8_t tx_address_width; // nRF905 Transmit address size (1-4 bytes) 61 | uint8_t tx_payload_width; // nRF905 Transmit payload size (1-32 bytes) 62 | ClkOut clkOutFrequency; // nRF905 clock out frequency 63 | bool clkOutEnable; // nRF905 clock out enabled: false=off, true=on 64 | uint32_t xtal_frequency; // nRF905 clock in frequency 65 | bool crc_enable; // nRF905 Enable CRC: false=CRC disabled, true=CRC enabled 66 | uint8_t crc_bits; // nRF905 CRC size: 8=8bit CRC, 16=16bit CRC 67 | uint32_t frequency; // Internal: RF frequency (internal use; not nRF905 register) --> TODO 68 | int8_t tx_power; // nRF905 Transmit power (-10dBm, -2dBm, 6dBm or 10dBm) 69 | } Config; 70 | 71 | typedef struct { 72 | uint8_t command; 73 | uint8_t data[NRF905_REGISTER_COUNT]; 74 | } ConfigBuffer; 75 | 76 | typedef struct { 77 | uint8_t command; 78 | uint8_t address[4]; 79 | } AddressBuffer; 80 | 81 | typedef struct { 82 | uint8_t command; 83 | uint8_t payload[NRF905_MAX_FRAMESIZE]; 84 | } Buffer; 85 | 86 | typedef std::function TxReadyCalllback; 87 | typedef std::function RxCompleteCallback; 88 | 89 | class nRF905 : public Component, 90 | public spi::SPIDevice { 92 | public: 93 | nRF905(); 94 | 95 | void setup() override; 96 | 97 | // float get_setup_priority() const override { return setup_priority::HARDWARE; } 98 | float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } 99 | 100 | void dump_config() override; 101 | void loop() override; 102 | 103 | void set_am_pin(GPIOPin *const pin) { _gpio_pin_am = pin; } 104 | void set_cd_pin(GPIOPin *const pin) { _gpio_pin_cd = pin; } 105 | void set_ce_pin(GPIOPin *const pin) { _gpio_pin_ce = pin; } 106 | void set_dr_pin(GPIOPin *const pin) { _gpio_pin_dr = pin; } 107 | void set_pwr_pin(GPIOPin *const pin) { _gpio_pin_pwr = pin; } 108 | void set_txen_pin(GPIOPin *const pin) { _gpio_pin_txen = pin; } 109 | 110 | void setOnRxComplete(RxCompleteCallback callback) { onRxComplete = callback; } 111 | void setOnTxReady(TxReadyCalllback callback) { onTxReady = callback; } 112 | 113 | Mode getMode(void) { return this->_mode; }; 114 | void setMode(const Mode mode); 115 | 116 | Config getConfig(void) { return this->_config; } 117 | void updateConfig(Config *config, uint8_t *const pStatus = NULL); 118 | 119 | void writeTxAddress(const uint32_t txAddress, uint8_t *const pStatus = NULL); 120 | void readTxAddress(uint32_t *const pTxAddress, uint8_t *const pStatus = NULL); 121 | 122 | void writeTxPayload(const uint8_t *const pData, const uint8_t dataLength, uint8_t *const pStatus = NULL); 123 | void readTxPayload(uint8_t *const pData, const uint8_t dataLength, uint8_t *const pStatus = NULL); 124 | 125 | bool airwayBusy(void); 126 | 127 | void startTx(const uint32_t retransmit, const Mode nextMode); 128 | 129 | void printConfig(const Config *const pConfig); 130 | 131 | protected: 132 | void readRxPayload(uint8_t *const pData, const uint8_t dataLength, uint8_t *const pStatus = NULL); 133 | 134 | void readConfigRegisters(uint8_t *const pStatus = NULL); 135 | void writeConfigRegisters(uint8_t *const pStatus = NULL); 136 | 137 | void decodeConfigRegisters(const ConfigBuffer *const pBuffer, Config *const pConfig); 138 | void encodeConfigRegisters(const Config *const pConfig, ConfigBuffer *const pBuffer); 139 | 140 | uint8_t readStatus(void); 141 | 142 | void spiTransfer(uint8_t *const data, const size_t length); 143 | 144 | char *hexArrayToStr(const uint8_t *const pData, const size_t dataLength); 145 | 146 | RxCompleteCallback onRxComplete{NULL}; 147 | 148 | uint32_t retransmitCounter{0}; 149 | Mode nextMode{PowerDown}; 150 | TxReadyCalllback onTxReady{NULL}; 151 | 152 | GPIOPin *_gpio_pin_am{NULL}; 153 | GPIOPin *_gpio_pin_cd{NULL}; 154 | GPIOPin *_gpio_pin_ce{NULL}; 155 | GPIOPin *_gpio_pin_dr{NULL}; 156 | GPIOPin *_gpio_pin_pwr{NULL}; 157 | GPIOPin *_gpio_pin_txen{NULL}; 158 | 159 | Mode _mode{PowerDown}; 160 | 161 | Config _config; 162 | }; 163 | 164 | } // namespace nrf905 165 | } // namespace esphome 166 | 167 | #endif /* __COMPONENT_nRF905_H__ */ 168 | -------------------------------------------------------------------------------- /components/nrf905/nRF905.cpp: -------------------------------------------------------------------------------- 1 | #include "nRF905.h" 2 | #include "esphome/core/log.h" 3 | 4 | #include 5 | 6 | #define CHECK_REG_WRITE true 7 | 8 | namespace esphome { 9 | namespace nrf905 { 10 | 11 | static const char *TAG = "nRF905"; 12 | 13 | nRF905::nRF905(void) {} 14 | 15 | void nRF905::setup() { 16 | Config config; 17 | 18 | ESP_LOGD(TAG, "Start nRF905 init"); 19 | 20 | this->spi_setup(); 21 | if (this->_gpio_pin_am != NULL) { 22 | this->_gpio_pin_am->setup(); 23 | } 24 | if (this->_gpio_pin_cd != NULL) { 25 | this->_gpio_pin_cd->setup(); 26 | } 27 | this->_gpio_pin_ce->setup(); 28 | if (this->_gpio_pin_dr != NULL) { 29 | this->_gpio_pin_dr->setup(); 30 | } 31 | this->_gpio_pin_pwr->setup(); 32 | this->_gpio_pin_txen->setup(); 33 | 34 | this->setMode(PowerDown); 35 | 36 | this->readConfigRegisters(); 37 | 38 | this->_config.band = true; 39 | this->_config.channel = 118; 40 | 41 | // CRC 16 42 | this->_config.crc_enable = true; 43 | this->_config.crc_bits = 16; 44 | 45 | // TX power 10 46 | this->_config.tx_power = 10; 47 | 48 | // RX power normal 49 | this->_config.rx_power = PowerNormal; 50 | 51 | this->_config.rx_address = 0x89816EA9; // ZEHNDER_NETWORK_LINK_ID; 52 | this->_config.rx_address_width = 4; 53 | this->_config.rx_payload_width = 16; 54 | 55 | this->_config.tx_address_width = 4; 56 | this->_config.tx_payload_width = 16; 57 | 58 | this->_config.xtal_frequency = 16000000; // defaults for now 59 | this->_config.clkOutFrequency = ClkOut500000; 60 | this->_config.clkOutEnable = false; 61 | 62 | // Write config back 63 | this->writeConfigRegisters(); 64 | this->writeTxAddress(0x89816EA9); 65 | 66 | // Return to idle 67 | this->setMode(Idle); 68 | 69 | ESP_LOGD(TAG, "nRF905 Setup complete"); 70 | } 71 | 72 | void nRF905::dump_config() { 73 | ESP_LOGCONFIG(TAG, "Config:"); 74 | 75 | LOG_PIN(" CS Pin:", this->cs_); 76 | if (this->_gpio_pin_am != NULL) { 77 | LOG_PIN(" AM Pin:", this->_gpio_pin_am); 78 | } 79 | if (this->_gpio_pin_dr != NULL) { 80 | LOG_PIN(" DR Pin:", this->_gpio_pin_dr); 81 | } 82 | if (this->_gpio_pin_cd != NULL) { 83 | LOG_PIN(" CD Pin:", this->_gpio_pin_cd); 84 | } 85 | LOG_PIN(" CE Pin:", this->_gpio_pin_ce); 86 | LOG_PIN(" PWR Pin:", this->_gpio_pin_pwr); 87 | LOG_PIN(" TXEN Pin:", this->_gpio_pin_txen); 88 | } 89 | 90 | void nRF905::loop() { 91 | static uint8_t lastState = 0x00; 92 | static bool addrMatch; 93 | uint8_t buffer[NRF905_MAX_FRAMESIZE]; 94 | 95 | uint8_t state = this->readStatus() & ((1 << NRF905_STATUS_DR) | (1 << NRF905_STATUS_AM)); 96 | if (lastState != state) { 97 | ESP_LOGV(TAG, "State change: 0x%02X -> 0x%02X", lastState, state); 98 | if (state == ((1 << NRF905_STATUS_DR) | (1 << NRF905_STATUS_AM))) { 99 | addrMatch = false; 100 | 101 | // Read data 102 | this->readRxPayload(buffer, NRF905_MAX_FRAMESIZE); 103 | ESP_LOGV(TAG, "RX Complete: %s", hexArrayToStr(buffer, NRF905_MAX_FRAMESIZE)); 104 | 105 | if (this->onRxComplete != NULL) { 106 | this->onRxComplete(buffer, NRF905_MAX_FRAMESIZE); 107 | } 108 | } else if (state == (1 << NRF905_STATUS_DR)) { 109 | addrMatch = false; 110 | 111 | // ESP_LOGD(TAG, "TX Ready; retransmits: %u", this->retransmitCounter); 112 | // if (this->retransmitCounter > 0) { 113 | // --this->retransmitCounter; 114 | // } else { 115 | this->setMode(this->nextMode); 116 | 117 | if (this->onTxReady != NULL) { 118 | this->onTxReady(); 119 | } 120 | // } 121 | } else if (state == (1 << NRF905_STATUS_AM)) { 122 | addrMatch = true; 123 | ESP_LOGD(TAG, "Addr match"); 124 | 125 | // if (onAddrMatch != NULL) 126 | // onAddrMatch(this); 127 | } else if (state == 0 && addrMatch) { 128 | addrMatch = false; 129 | ESP_LOGD(TAG, "Rx Invalid"); 130 | // if (onRxInvalid != NULL) 131 | // onRxInvalid(this); 132 | } 133 | 134 | lastState = state; 135 | } 136 | 137 | // _drPrev = _drNew; 138 | } 139 | 140 | void nRF905::setMode(const Mode mode) { 141 | // Set power 142 | switch (mode) { 143 | case PowerDown: 144 | this->_gpio_pin_pwr->digital_write(false); 145 | break; 146 | 147 | default: 148 | this->_gpio_pin_pwr->digital_write(true); 149 | break; 150 | } 151 | 152 | // Set CE 153 | switch (mode) { 154 | case Receive: // fall through 155 | case Transmit: 156 | this->_gpio_pin_ce->digital_write(true); 157 | break; 158 | 159 | default: 160 | this->_gpio_pin_ce->digital_write(false); 161 | break; 162 | } 163 | 164 | // Enable TX 165 | switch (mode) { 166 | case Transmit: 167 | this->_gpio_pin_txen->digital_write(true); 168 | break; 169 | 170 | default: 171 | this->_gpio_pin_txen->digital_write(false); 172 | break; 173 | } 174 | 175 | this->_mode = mode; 176 | } 177 | 178 | void nRF905::updateConfig(Config *config, uint8_t *const pStatus) { 179 | this->_config = *config; 180 | 181 | this->writeConfigRegisters(pStatus); 182 | } 183 | 184 | void nRF905::readConfigRegisters(uint8_t *const pStatus) { 185 | Mode mode; 186 | ConfigBuffer buffer; 187 | 188 | // Set mode to idle 189 | mode = this->_mode; 190 | this->setMode(Idle); 191 | 192 | // Prepare data 193 | buffer.command = NRF905_COMMAND_R_CONFIG; 194 | (void) memset(buffer.data, 0, sizeof(buffer.data)); 195 | 196 | // Transfer 197 | this->spiTransfer((uint8_t *) &buffer, sizeof(ConfigBuffer)); 198 | if (pStatus != NULL) { 199 | *pStatus = buffer.command; 200 | } 201 | 202 | // Ccear 203 | (void) memset(&this->_config, 0, sizeof(Config)); 204 | this->decodeConfigRegisters(&buffer, &this->_config); 205 | 206 | // Restore mode 207 | this->setMode(mode); 208 | } 209 | 210 | void nRF905::writeConfigRegisters(uint8_t *const pStatus) { 211 | Mode mode; 212 | ConfigBuffer buffer; 213 | #if CHECK_REG_WRITE 214 | uint8_t writeData[NRF905_REGISTER_COUNT]; 215 | #endif 216 | 217 | mode = this->_mode; 218 | this->setMode(Idle); 219 | 220 | this->printConfig(&this->_config); 221 | 222 | // Create data 223 | buffer.command = NRF905_COMMAND_W_CONFIG; 224 | this->encodeConfigRegisters(&this->_config, &buffer); 225 | 226 | ESP_LOGV(TAG, "Write config data: %s", hexArrayToStr(buffer.data, NRF905_REGISTER_COUNT)); 227 | #if CHECK_REG_WRITE 228 | (void) memcpy(writeData, buffer.data, NRF905_REGISTER_COUNT); 229 | #endif 230 | 231 | this->spiTransfer((uint8_t *) &buffer, sizeof(ConfigBuffer)); 232 | 233 | #if CHECK_REG_WRITE 234 | // Check config write by reading config back and compare 235 | { 236 | ConfigBuffer bufferRead; 237 | 238 | bufferRead.command = NRF905_COMMAND_R_CONFIG; 239 | (void) memset(bufferRead.data, 0, NRF905_REGISTER_COUNT); 240 | 241 | this->spiTransfer((uint8_t *) &bufferRead, sizeof(ConfigBuffer)); 242 | if (memcmp((void *) writeData, (void *) bufferRead.data, NRF905_REGISTER_COUNT) != 0) { 243 | ESP_LOGE(TAG, "Config write failed"); 244 | } else { 245 | ESP_LOGV(TAG, "Write config OK"); 246 | } 247 | } 248 | #endif 249 | 250 | if (pStatus != NULL) { 251 | *pStatus = buffer.command; 252 | } 253 | 254 | // Restore mode 255 | this->setMode(mode); 256 | } 257 | 258 | void nRF905::writeTxAddress(const uint32_t txAddress, uint8_t *const pStatus) { 259 | Mode mode; 260 | AddressBuffer buffer; 261 | 262 | ESP_LOGD(TAG, "Set TX Address: 0x%08X", txAddress); 263 | 264 | mode = this->_mode; 265 | this->setMode(Idle); 266 | 267 | buffer.command = NRF905_COMMAND_W_TX_ADDRESS; 268 | buffer.address[3] = (txAddress >> 24) & 0xFF; 269 | buffer.address[2] = (txAddress >> 16) & 0xFF; 270 | buffer.address[1] = (txAddress >> 8) & 0xFF; 271 | buffer.address[0] = (txAddress) &0xFF; 272 | 273 | this->spiTransfer((uint8_t *) &buffer, sizeof(AddressBuffer)); 274 | 275 | if (pStatus != NULL) { 276 | *pStatus = buffer.command; 277 | } 278 | 279 | // Restore mode 280 | this->setMode(mode); 281 | } 282 | 283 | void nRF905::readTxAddress(uint32_t *pTxAddress, uint8_t *const pStatus) { 284 | Mode mode; 285 | AddressBuffer buffer; 286 | 287 | mode = this->_mode; 288 | this->setMode(Idle); 289 | 290 | buffer.command = NRF905_COMMAND_R_TX_ADDRESS; 291 | (void) memset(buffer.address, 0, 4); 292 | 293 | this->spiTransfer((uint8_t *) &buffer, sizeof(AddressBuffer)); 294 | 295 | *pTxAddress = buffer.address[0]; 296 | *pTxAddress |= (buffer.address[1] << 8); 297 | *pTxAddress |= (buffer.address[2] << 16); 298 | *pTxAddress |= (buffer.address[3] << 24); 299 | 300 | ESP_LOGD(TAG, "Got TX Address: 0x%08X", *pTxAddress); 301 | 302 | if (pStatus != NULL) { 303 | *pStatus = buffer.command; 304 | } 305 | 306 | this->setMode(mode); 307 | } 308 | 309 | void nRF905::readTxPayload(uint8_t *const pData, const uint8_t dataLength, uint8_t *const pStatus) { 310 | Mode mode; 311 | Buffer buffer; 312 | 313 | if (pData == NULL) { 314 | ESP_LOGE(TAG, "Read TX payload data pointer invalid"); 315 | return; 316 | } 317 | if (dataLength > NRF905_MAX_FRAMESIZE) { 318 | ESP_LOGE(TAG, "Read TX payload data length invalid"); 319 | return; 320 | } 321 | 322 | buffer.command = NRF905_COMMAND_R_TX_PAYLOAD; 323 | (void) memset(buffer.payload, 0, NRF905_MAX_FRAMESIZE); 324 | 325 | mode = this->_mode; 326 | this->setMode(Idle); 327 | 328 | this->spiTransfer((uint8_t *) &buffer, sizeof(Buffer)); 329 | (void) memcpy(pData, buffer.payload, dataLength); 330 | 331 | if (pStatus != NULL) { 332 | *pStatus = buffer.command; 333 | } 334 | 335 | this->setMode(mode); 336 | } 337 | 338 | void nRF905::writeTxPayload(const uint8_t *const pData, const uint8_t dataLength, uint8_t *const pStatus) { 339 | Mode mode; 340 | Buffer buffer; 341 | 342 | if (pData == NULL) { 343 | ESP_LOGE(TAG, "Write data pointer invalid"); 344 | return; 345 | } 346 | if (dataLength > NRF905_MAX_FRAMESIZE) { 347 | ESP_LOGE(TAG, "Write data length invalid"); 348 | return; 349 | } 350 | 351 | ESP_LOGV(TAG, "Read config data: %s", hexArrayToStr(pData, dataLength)); 352 | 353 | // Clear buffer payload 354 | (void) memset(buffer.payload, 0, NRF905_MAX_FRAMESIZE); 355 | 356 | buffer.command = NRF905_COMMAND_W_TX_PAYLOAD; 357 | (void) memcpy(buffer.payload, (uint8_t *) pData, dataLength); 358 | 359 | mode = this->_mode; 360 | this->setMode(Idle); 361 | 362 | this->spiTransfer((uint8_t *) &buffer, sizeof(Buffer)); 363 | if (pStatus != NULL) { 364 | *pStatus = buffer.command; 365 | } 366 | 367 | this->setMode(mode); 368 | } 369 | 370 | void nRF905::readRxPayload(uint8_t *const pData, const uint8_t dataLength, uint8_t *const pStatus) { 371 | Buffer buffer; 372 | 373 | if (pData == NULL) { 374 | ESP_LOGE(TAG, "Read RX data pointer invalid"); 375 | return; 376 | } 377 | if (dataLength > NRF905_MAX_FRAMESIZE) { 378 | ESP_LOGE(TAG, "Read RX data length invalid"); 379 | return; 380 | } 381 | 382 | buffer.command = NRF905_COMMAND_R_RX_PAYLOAD; 383 | (void) memset(buffer.payload, 0, NRF905_MAX_FRAMESIZE); 384 | 385 | this->spiTransfer((uint8_t *) &buffer, sizeof(Buffer)); 386 | 387 | (void) memcpy(pData, buffer.payload, dataLength); 388 | 389 | // Return status if needed 390 | if (pStatus != NULL) { 391 | *pStatus = buffer.command; 392 | } 393 | } 394 | 395 | void nRF905::decodeConfigRegisters(const ConfigBuffer *const pBuffer, Config *const pConfig) { 396 | pConfig->channel = ((pBuffer->data[1] & 0x01) << 8) | pBuffer->data[0]; 397 | pConfig->band = (pBuffer->data[1] & 0x02) ? true : false; 398 | pConfig->rx_power = (RxPower)(pBuffer->data[1] & 0x10); 399 | pConfig->auto_retransmit = (pBuffer->data[1] & 0x20) ? true : false; 400 | pConfig->rx_address_width = pBuffer->data[2] & 0x07; 401 | pConfig->tx_address_width = (pBuffer->data[2] >> 4) & 0x07; 402 | pConfig->rx_payload_width = pBuffer->data[3] & 0x3F; 403 | pConfig->tx_payload_width = pBuffer->data[4] & 0x3F; 404 | pConfig->rx_address = 405 | ((pBuffer->data[8] << 24) | (pBuffer->data[7] << 16) | (pBuffer->data[6] << 8) | pBuffer->data[5]); 406 | pConfig->clkOutFrequency = (ClkOut)(pBuffer->data[9] & 0x03); 407 | pConfig->clkOutEnable = (pBuffer->data[9] & 0x04) ? true : false; 408 | pConfig->xtal_frequency = (((pBuffer->data[9] >> 3) & 0x07) + 1) * 4000000; 409 | pConfig->crc_enable = (pBuffer->data[9] & 0x40) ? true : false; 410 | pConfig->crc_bits = (pBuffer->data[9] & 0x80) ? 16 : 8; 411 | 412 | pConfig->frequency = ((422400000 + (pConfig->channel * 100000)) * (pConfig->band ? 2 : 1)); // internal 413 | switch ((pBuffer->data[1] >> 2) & 0x03) { 414 | case 0x00: 415 | pConfig->tx_power = -10; 416 | break; 417 | 418 | case 0x01: 419 | pConfig->tx_power = -2; 420 | break; 421 | 422 | case 0x02: 423 | pConfig->tx_power = 6; 424 | break; 425 | 426 | case 0x03: 427 | pConfig->tx_power = 10; 428 | break; 429 | 430 | default: 431 | pConfig->tx_power = 10; 432 | break; 433 | } 434 | } 435 | 436 | void nRF905::encodeConfigRegisters(const Config *const pConfig, ConfigBuffer *const pBuffer) { 437 | uint8_t tx_power; 438 | 439 | switch (pConfig->tx_power) { 440 | case -10: 441 | tx_power = 0x00; 442 | break; 443 | 444 | case -2: 445 | tx_power = 0x04; 446 | break; 447 | 448 | case 6: 449 | tx_power = 0x08; 450 | break; 451 | 452 | case 10: 453 | tx_power = 0x0C; 454 | break; 455 | 456 | default: 457 | tx_power = 0x0C; 458 | break; 459 | } 460 | 461 | pBuffer->data[0] = (pConfig->channel & 0xFF); 462 | pBuffer->data[1] = (pConfig->channel >> 8) & 0x01; 463 | pBuffer->data[1] |= (pConfig->band ? 0x02 : 0x00); 464 | pBuffer->data[1] |= tx_power; 465 | pBuffer->data[1] |= (pConfig->rx_power == PowerReduced ? 0x10 : 0x00); 466 | pBuffer->data[1] |= (pConfig->auto_retransmit ? 0x20 : 0x00); 467 | pBuffer->data[2] = (pConfig->rx_address_width & 0x07); 468 | pBuffer->data[2] |= (pConfig->tx_address_width & 0x07) << 4; 469 | pBuffer->data[3] = (pConfig->rx_payload_width & 0x3F); 470 | pBuffer->data[4] = (pConfig->tx_payload_width & 0x3F); 471 | pBuffer->data[5] = (pConfig->rx_address & 0xFF); 472 | pBuffer->data[6] = (pConfig->rx_address >> 8) & 0xFF; 473 | pBuffer->data[7] = (pConfig->rx_address >> 16) & 0xFF; 474 | pBuffer->data[8] = (pConfig->rx_address >> 24) & 0xFF; 475 | pBuffer->data[9] = pConfig->clkOutFrequency; // use enum value 476 | pBuffer->data[9] |= (pConfig->clkOutEnable ? 0x04 : 0x00); 477 | pBuffer->data[9] |= ((pConfig->xtal_frequency / 4000000) - 1) << 3; 478 | pBuffer->data[9] |= (pConfig->crc_enable ? 0x40 : 0x00); 479 | pBuffer->data[9] |= (pConfig->crc_bits == 8) ? 0x00 : 0x80; 480 | } 481 | 482 | void nRF905::printConfig(const Config *const pConfig) { 483 | uint32_t hz = 0; 484 | 485 | switch (pConfig->clkOutFrequency) { 486 | case ClkOut500000: 487 | hz = 500000; 488 | break; 489 | 490 | case ClkOut1000000: 491 | hz = 1000000; 492 | break; 493 | 494 | case ClkOut2000000: 495 | hz = 2000000; 496 | break; 497 | 498 | case ClkOut4000000: 499 | hz = 4000000; 500 | break; 501 | 502 | default: 503 | ESP_LOGD(TAG, "Unvalid clock freq"); 504 | break; 505 | } 506 | 507 | ESP_LOGV(TAG, 508 | "Config:\r\n" 509 | " Channel %u Band %s MHz -> %u\r\n" 510 | " Rx Power %s\r\n" 511 | " Tx Retransmit %s\r\n" 512 | " Rx Address (%u) 0x%08X\r\n" 513 | " Rx Payload width %u\r\n" 514 | " Tx Address (%u)\r\n" 515 | " Tx Payload width %u\r\n" 516 | " Clk Out %u\r\n" 517 | " XTAL Freq %u\r\n" 518 | " CRC %s -> %u\r\n" 519 | " TX Power %d dBm", 520 | pConfig->channel, pConfig->band ? "868" : "434", pConfig->frequency, 521 | pConfig->rx_power ? "reduced" : "normal", pConfig->auto_retransmit ? "On" : "Off", pConfig->rx_address_width, 522 | pConfig->rx_address, pConfig->rx_payload_width, pConfig->tx_address_width, pConfig->tx_payload_width, hz, 523 | pConfig->xtal_frequency, pConfig->crc_enable ? "On" : "Off", pConfig->crc_bits, pConfig->tx_power); 524 | } 525 | 526 | bool nRF905::airwayBusy(void) { 527 | bool busy = false; 528 | 529 | if (this->_gpio_pin_cd != NULL) { 530 | busy = this->_gpio_pin_cd->digital_read() == true; 531 | } 532 | 533 | return busy; 534 | } 535 | 536 | void nRF905::startTx(const uint32_t retransmit, const Mode nextMode) { 537 | bool update = false; 538 | if (this->_mode == PowerDown) { 539 | this->setMode(Idle); 540 | delay(3); // Delay is needed to the radio has time to power-up and see the standby/TX pins pulse 541 | } 542 | 543 | // Update counters 544 | // this->retransmitCounter = retransmit; 545 | this->nextMode = nextMode; 546 | 547 | // Set or clear retransmit flag 548 | // if ((this->_config.auto_retransmit == false) && (retransmit > 0)) { 549 | // this->_config.auto_retransmit = true; 550 | // update = true; 551 | // } else if ((this->_config.auto_retransmit == true) && (retransmit == 0)) { 552 | this->_config.auto_retransmit = false; 553 | update = true; 554 | // } 555 | if (update == true) { 556 | this->writeConfigRegisters(); 557 | } 558 | 559 | // Start transmit 560 | this->setMode(Transmit); 561 | } 562 | 563 | uint8_t nRF905::readStatus(void) { 564 | uint8_t status = 0; 565 | 566 | status = NRF905_COMMAND_NOP; 567 | 568 | this->spiTransfer(&status, 1); 569 | 570 | return status; 571 | } 572 | 573 | void nRF905::spiTransfer(uint8_t *const data, const size_t length) { 574 | this->enable(); 575 | 576 | this->transfer_array(data, length); 577 | 578 | this->disable(); 579 | } 580 | 581 | char *nRF905::hexArrayToStr(const uint8_t *const pData, const size_t dataLength) { 582 | static char buf[256]; 583 | size_t bufIdx = 0; 584 | 585 | for (size_t i = 0; i < dataLength; ++i) { 586 | if (i > 0) { 587 | bufIdx += snprintf(&buf[bufIdx], 256 - bufIdx, " "); 588 | } 589 | bufIdx += snprintf(&buf[bufIdx], 256 - bufIdx, "0x%02X", pData[i]); 590 | } 591 | 592 | return buf; 593 | } 594 | 595 | } // namespace nrf905 596 | } // namespace esphome 597 | -------------------------------------------------------------------------------- /components/zehnder/zehnder.cpp: -------------------------------------------------------------------------------- 1 | #include "zehnder.h" 2 | #include "esphome/core/log.h" 3 | #include "esphome/core/application.h" 4 | 5 | namespace esphome { 6 | namespace zehnder { 7 | 8 | #define MAX_TRANSMIT_TIME 2000 9 | 10 | static const char *const TAG = "zehnder"; 11 | 12 | typedef struct __attribute__((packed)) { 13 | uint32_t networkId; 14 | } RfPayloadNetworkJoinOpen; 15 | 16 | typedef struct __attribute__((packed)) { 17 | uint32_t networkId; 18 | } RfPayloadNetworkJoinRequest; 19 | 20 | typedef struct __attribute__((packed)) { 21 | uint32_t networkId; 22 | } RfPayloadNetworkJoinAck; 23 | 24 | typedef struct __attribute__((packed)) { 25 | uint8_t speed; 26 | uint8_t voltage; 27 | uint8_t timer; 28 | } RfPayloadFanSettings; 29 | 30 | typedef struct __attribute__((packed)) { 31 | uint8_t speed; 32 | } RfPayloadFanSetSpeed; 33 | 34 | typedef struct __attribute__((packed)) { 35 | uint8_t speed; 36 | uint8_t timer; 37 | } RfPayloadFanSetTimer; 38 | 39 | typedef struct __attribute__((packed)) { 40 | uint8_t rx_type; // 0x00 RX Type 41 | uint8_t rx_id; // 0x01 RX ID 42 | uint8_t tx_type; // 0x02 TX Type 43 | uint8_t tx_id; // 0x03 TX ID 44 | uint8_t ttl; // 0x04 Time-To-Live 45 | uint8_t command; // 0x05 Frame type 46 | uint8_t parameter_count; // 0x06 Number of parameters 47 | 48 | union { 49 | uint8_t parameters[9]; // 0x07 - 0x0F Depends on command 50 | RfPayloadFanSetSpeed setSpeed; // Command 0x02 51 | RfPayloadFanSetTimer setTimer; // Command 0x03 52 | RfPayloadNetworkJoinRequest networkJoinRequest; // Command 0x04 53 | RfPayloadNetworkJoinOpen networkJoinOpen; // Command 0x06 54 | RfPayloadFanSettings fanSettings; // Command 0x07 55 | RfPayloadNetworkJoinAck networkJoinAck; // Command 0x0C 56 | } payload; 57 | } RfFrame; 58 | 59 | ZehnderRF::ZehnderRF(void) {} 60 | 61 | fan::FanTraits ZehnderRF::get_traits() { return fan::FanTraits(false, true, false, this->speed_count_); } 62 | 63 | void ZehnderRF::control(const fan::FanCall &call) { 64 | if (call.get_state().has_value()) { 65 | this->state = *call.get_state(); 66 | ESP_LOGD(TAG, "Control has state: %u", this->state); 67 | } 68 | if (call.get_speed().has_value()) { 69 | this->speed = *call.get_speed(); 70 | ESP_LOGD(TAG, "Control has speed: %u", this->speed); 71 | } 72 | 73 | switch (this->state_) { 74 | case StateIdle: 75 | // Set speed 76 | this->setSpeed(this->state ? this->speed : 0x00, 0); 77 | 78 | this->lastFanQuery_ = millis(); // Update time 79 | break; 80 | 81 | default: 82 | break; 83 | } 84 | 85 | this->publish_state(); 86 | } 87 | 88 | void ZehnderRF::setup() { 89 | ESP_LOGCONFIG(TAG, "ZEHNDER '%s':", this->get_name().c_str()); 90 | 91 | // Clear config 92 | memset(&this->config_, 0, sizeof(Config)); 93 | 94 | uint32_t hash = fnv1_hash("zehnderrf"); 95 | this->pref_ = global_preferences->make_preference(hash, true); 96 | if (this->pref_.load(&this->config_)) { 97 | ESP_LOGD(TAG, "Config load ok"); 98 | } 99 | 100 | // Set nRF905 config 101 | nrf905::Config rfConfig; 102 | rfConfig = this->rf_->getConfig(); 103 | 104 | rfConfig.band = true; 105 | rfConfig.channel = 118; 106 | 107 | // // CRC 16 108 | rfConfig.crc_enable = true; 109 | rfConfig.crc_bits = 16; 110 | 111 | // // TX power 10 112 | rfConfig.tx_power = 10; 113 | 114 | // // RX power normal 115 | rfConfig.rx_power = nrf905::PowerNormal; 116 | 117 | rfConfig.rx_address = 0x89816EA9; // ZEHNDER_NETWORK_LINK_ID; 118 | rfConfig.rx_address_width = 4; 119 | rfConfig.rx_payload_width = 16; 120 | 121 | rfConfig.tx_address_width = 4; 122 | rfConfig.tx_payload_width = 16; 123 | 124 | rfConfig.xtal_frequency = 16000000; // defaults for now 125 | rfConfig.clkOutFrequency = nrf905::ClkOut500000; 126 | rfConfig.clkOutEnable = false; 127 | 128 | // Write config back 129 | this->rf_->updateConfig(&rfConfig); 130 | this->rf_->writeTxAddress(0x89816EA9); 131 | 132 | this->speed_count_ = 4; 133 | 134 | this->rf_->setOnTxReady([this](void) { 135 | ESP_LOGD(TAG, "Tx Ready"); 136 | if (this->rfState_ == RfStateTxBusy) { 137 | if (this->retries_ >= 0) { 138 | this->msgSendTime_ = millis(); 139 | this->rfState_ = RfStateRxWait; 140 | } else { 141 | this->rfState_ = RfStateIdle; 142 | } 143 | } 144 | }); 145 | 146 | this->rf_->setOnRxComplete([this](const uint8_t *const pData, const uint8_t dataLength) { 147 | ESP_LOGV(TAG, "Received frame"); 148 | this->rfHandleReceived(pData, dataLength); 149 | }); 150 | } 151 | 152 | void ZehnderRF::dump_config(void) { 153 | ESP_LOGCONFIG(TAG, "Zehnder Fan config:"); 154 | ESP_LOGCONFIG(TAG, " Polling interval %u", this->interval_); 155 | ESP_LOGCONFIG(TAG, " Fan networkId 0x%08X", this->config_.fan_networkId); 156 | ESP_LOGCONFIG(TAG, " Fan my device type 0x%02X", this->config_.fan_my_device_type); 157 | ESP_LOGCONFIG(TAG, " Fan my device id 0x%02X", this->config_.fan_my_device_id); 158 | ESP_LOGCONFIG(TAG, " Fan main_unit type 0x%02X", this->config_.fan_main_unit_type); 159 | ESP_LOGCONFIG(TAG, " Fan main unit id 0x%02X", this->config_.fan_main_unit_id); 160 | } 161 | 162 | void ZehnderRF::loop(void) { 163 | uint8_t deviceId; 164 | nrf905::Config rfConfig; 165 | 166 | // Run RF handler 167 | this->rfHandler(); 168 | 169 | switch (this->state_) { 170 | case StateStartup: 171 | // Wait until started up 172 | if (millis() > 15000) { 173 | // Discovery? 174 | if ((this->config_.fan_networkId == 0x00000000) || (this->config_.fan_my_device_type == 0) || 175 | (this->config_.fan_my_device_id == 0) || (this->config_.fan_main_unit_type == 0) || 176 | (this->config_.fan_main_unit_id == 0)) { 177 | ESP_LOGD(TAG, "Invalid config, start paring"); 178 | 179 | this->state_ = StateStartDiscovery; 180 | } else { 181 | ESP_LOGD(TAG, "Config data valid, start polling"); 182 | 183 | rfConfig = this->rf_->getConfig(); 184 | rfConfig.rx_address = this->config_.fan_networkId; 185 | this->rf_->updateConfig(&rfConfig); 186 | this->rf_->writeTxAddress(this->config_.fan_networkId); 187 | 188 | // Start with query 189 | this->queryDevice(); 190 | } 191 | } 192 | break; 193 | 194 | case StateStartDiscovery: 195 | deviceId = this->createDeviceID(); 196 | this->discoveryStart(deviceId); 197 | 198 | // For now just set TX 199 | break; 200 | 201 | case StateIdle: 202 | if (newSetting == true) { 203 | this->setSpeed(newSpeed, newTimer); 204 | } else { 205 | if ((millis() - this->lastFanQuery_) > this->interval_) { 206 | this->queryDevice(); 207 | } 208 | } 209 | break; 210 | 211 | case StateWaitSetSpeedConfirm: 212 | if (this->rfState_ == RfStateIdle) { 213 | // When done, return to idle 214 | this->state_ = StateIdle; 215 | } 216 | 217 | default: 218 | break; 219 | } 220 | } 221 | 222 | void ZehnderRF::rfHandleReceived(const uint8_t *const pData, const uint8_t dataLength) { 223 | const RfFrame *const pResponse = (RfFrame *) pData; 224 | RfFrame *const pTxFrame = (RfFrame *) this->_txFrame; // frame helper 225 | nrf905::Config rfConfig; 226 | 227 | ESP_LOGD(TAG, "Current state: 0x%02X", this->state_); 228 | switch (this->state_) { 229 | case StateDiscoveryWaitForLinkRequest: 230 | ESP_LOGD(TAG, "DiscoverStateWaitForLinkRequest"); 231 | switch (pResponse->command) { 232 | case FAN_NETWORK_JOIN_OPEN: // Received linking request from main unit 233 | ESP_LOGD(TAG, "Discovery: Found unit type 0x%02X (%s) with ID 0x%02X on network 0x%08X", pResponse->tx_type, 234 | pResponse->tx_type == FAN_TYPE_MAIN_UNIT ? "Main" : "?", pResponse->tx_id, 235 | pResponse->payload.networkJoinOpen.networkId); 236 | 237 | this->rfComplete(); 238 | 239 | (void) memset(this->_txFrame, 0, FAN_FRAMESIZE); // Clear frame data 240 | 241 | // Found a main unit, so send a join request 242 | pTxFrame->rx_type = FAN_TYPE_MAIN_UNIT; // Set type to main unit 243 | pTxFrame->rx_id = pResponse->tx_id; // Set ID to the ID of the main unit 244 | pTxFrame->tx_type = this->config_.fan_my_device_type; 245 | pTxFrame->tx_id = this->config_.fan_my_device_id; 246 | pTxFrame->ttl = FAN_TTL; 247 | pTxFrame->command = FAN_NETWORK_JOIN_REQUEST; // Request to connect to network 248 | pTxFrame->parameter_count = sizeof(RfPayloadNetworkJoinOpen); 249 | // Request to connect to the received network ID 250 | pTxFrame->payload.networkJoinRequest.networkId = pResponse->payload.networkJoinOpen.networkId; 251 | 252 | // Store for later 253 | this->config_.fan_networkId = pResponse->payload.networkJoinOpen.networkId; 254 | this->config_.fan_main_unit_type = pResponse->tx_type; 255 | this->config_.fan_main_unit_id = pResponse->tx_id; 256 | 257 | // Update address 258 | rfConfig = this->rf_->getConfig(); 259 | rfConfig.rx_address = pResponse->payload.networkJoinOpen.networkId; 260 | this->rf_->updateConfig(&rfConfig, NULL); 261 | this->rf_->writeTxAddress(pResponse->payload.networkJoinOpen.networkId, NULL); 262 | 263 | // Send response frame 264 | this->startTransmit(this->_txFrame, FAN_TX_RETRIES, [this]() { 265 | ESP_LOGW(TAG, "Query Timeout"); 266 | this->state_ = StateStartDiscovery; 267 | }); 268 | 269 | this->state_ = StateDiscoveryWaitForJoinResponse; 270 | break; 271 | 272 | default: 273 | ESP_LOGD(TAG, "Discovery: Received unknown frame type 0x%02X from ID 0x%02X", pResponse->command, 274 | pResponse->tx_id); 275 | break; 276 | } 277 | break; 278 | 279 | case StateDiscoveryWaitForJoinResponse: 280 | ESP_LOGD(TAG, "DiscoverStateWaitForJoinResponse"); 281 | switch (pResponse->command) { 282 | case FAN_FRAME_0B: 283 | if ((pResponse->rx_type == this->config_.fan_my_device_type) && 284 | (pResponse->rx_id == this->config_.fan_my_device_id) && 285 | (pResponse->tx_type == this->config_.fan_main_unit_type) && 286 | (pResponse->tx_id == this->config_.fan_main_unit_id)) { 287 | ESP_LOGD(TAG, "Discovery: Link successful to unit with ID 0x%02X on network 0x%08X", pResponse->tx_id, 288 | this->config_.fan_networkId); 289 | 290 | this->rfComplete(); 291 | 292 | (void) memset(this->_txFrame, 0, FAN_FRAMESIZE); // Clear frame data 293 | 294 | pTxFrame->rx_type = FAN_TYPE_MAIN_UNIT; // Set type to main unit 295 | pTxFrame->rx_id = pResponse->tx_id; // Set ID to the ID of the main unit 296 | pTxFrame->tx_type = this->config_.fan_my_device_type; 297 | pTxFrame->tx_id = this->config_.fan_my_device_id; 298 | pTxFrame->ttl = FAN_TTL; 299 | pTxFrame->command = FAN_FRAME_0B; // 0x0B acknowledge link successful 300 | pTxFrame->parameter_count = 0x00; // No parameters 301 | 302 | // Send response frame 303 | this->startTransmit(this->_txFrame, FAN_TX_RETRIES, [this]() { 304 | ESP_LOGW(TAG, "Query Timeout"); 305 | this->state_ = StateStartDiscovery; 306 | }); 307 | 308 | this->state_ = StateDiscoveryJoinComplete; 309 | } else { 310 | ESP_LOGE(TAG, "Discovery: Received unknown link success from ID 0x%02X on network 0x%08X", pResponse->tx_id, 311 | this->config_.fan_networkId); 312 | } 313 | break; 314 | 315 | default: 316 | ESP_LOGE(TAG, "Discovery: Received unknown frame type 0x%02X from ID 0x%02X", pResponse->command, 317 | pResponse->tx_id); 318 | break; 319 | } 320 | break; 321 | 322 | case StateDiscoveryJoinComplete: 323 | ESP_LOGD(TAG, "StateDiscoveryJoinComplete"); 324 | switch (pResponse->command) { 325 | case FAN_TYPE_QUERY_NETWORK: 326 | if ((pResponse->rx_type == this->config_.fan_main_unit_type) && 327 | (pResponse->rx_id == this->config_.fan_main_unit_id) && 328 | (pResponse->tx_type == this->config_.fan_main_unit_type) && 329 | (pResponse->tx_id == this->config_.fan_main_unit_id)) { 330 | ESP_LOGD(TAG, "Discovery: received network join success 0x0D"); 331 | 332 | this->rfComplete(); 333 | 334 | ESP_LOGD(TAG, "Saving pairing config"); 335 | this->pref_.save(&this->config_); 336 | 337 | this->state_ = StateIdle; 338 | } else { 339 | ESP_LOGW(TAG, "Unexpected frame join reponse from Type 0x%02X ID 0x%02X", pResponse->tx_type, 340 | pResponse->tx_id); 341 | } 342 | break; 343 | 344 | default: 345 | ESP_LOGE(TAG, "Discovery: Received unknown frame type 0x%02X from ID 0x%02X on network 0x%08X", 346 | pResponse->command, pResponse->tx_id, this->config_.fan_networkId); 347 | break; 348 | } 349 | break; 350 | 351 | case StateWaitQueryResponse: 352 | if ((pResponse->rx_type == this->config_.fan_my_device_type) && // If type 353 | (pResponse->rx_id == this->config_.fan_my_device_id)) { // and id match, it is for us 354 | switch (pResponse->command) { 355 | case FAN_TYPE_FAN_SETTINGS: 356 | ESP_LOGD(TAG, "Received fan settings; speed: 0x%02X voltage: %i timer: %i", 357 | pResponse->payload.fanSettings.speed, pResponse->payload.fanSettings.voltage, 358 | pResponse->payload.fanSettings.timer); 359 | 360 | this->rfComplete(); 361 | 362 | this->state = pResponse->payload.fanSettings.speed > 0; 363 | this->speed = pResponse->payload.fanSettings.speed; 364 | this->publish_state(); 365 | 366 | this->state_ = StateIdle; 367 | break; 368 | 369 | default: 370 | ESP_LOGD(TAG, "Received unexpected frame; type 0x%02X from ID 0x%02X", pResponse->command, 371 | pResponse->tx_id); 372 | break; 373 | } 374 | } else { 375 | ESP_LOGD(TAG, "Received frame from unknown device; type 0x%02X from ID 0x%02X type 0x%02X", pResponse->command, 376 | pResponse->tx_id, pResponse->tx_type); 377 | } 378 | break; 379 | 380 | case StateWaitSetSpeedResponse: 381 | if ((pResponse->rx_type == this->config_.fan_my_device_type) && // If type 382 | (pResponse->rx_id == this->config_.fan_my_device_id)) { // and id match, it is for us 383 | switch (pResponse->command) { 384 | case FAN_TYPE_FAN_SETTINGS: 385 | ESP_LOGD(TAG, "Received fan settings; speed: 0x%02X voltage: %i timer: %i", 386 | pResponse->payload.fanSettings.speed, pResponse->payload.fanSettings.voltage, 387 | pResponse->payload.fanSettings.timer); 388 | this->rfComplete(); 389 | 390 | this->rfComplete(); 391 | 392 | (void) memset(this->_txFrame, 0, FAN_FRAMESIZE); // Clear frame data 393 | 394 | pTxFrame->rx_type = this->config_.fan_main_unit_type; // Set type to main unit 395 | pTxFrame->rx_id = this->config_.fan_main_unit_id; // Set ID to the ID of the main unit 396 | pTxFrame->tx_type = this->config_.fan_my_device_type; 397 | pTxFrame->tx_id = this->config_.fan_my_device_id; 398 | pTxFrame->ttl = FAN_TTL; 399 | pTxFrame->command = FAN_FRAME_SETSPEED_REPLY; // 0x0B acknowledge link successful 400 | pTxFrame->parameter_count = 0x03; // 3 parameters 401 | pTxFrame->payload.parameters[0] = 0x54; 402 | pTxFrame->payload.parameters[1] = 0x03; 403 | pTxFrame->payload.parameters[2] = 0x20; 404 | 405 | // Send response frame 406 | this->startTransmit(this->_txFrame, -1, NULL); 407 | 408 | this->state_ = StateWaitSetSpeedConfirm; 409 | break; 410 | 411 | case FAN_FRAME_SETSPEED_REPLY: 412 | case FAN_FRAME_SETVOLTAGE_REPLY: 413 | // this->rfComplete(); 414 | 415 | // this->state_ = StateIdle; 416 | break; 417 | 418 | default: 419 | ESP_LOGD(TAG, "Received unexpected frame; type 0x%02X from ID 0x%02X", pResponse->command, 420 | pResponse->tx_id); 421 | break; 422 | } 423 | } else { 424 | ESP_LOGD(TAG, "Received frame from unknown device; type 0x%02X from ID 0x%02X type 0x%02X", pResponse->command, 425 | pResponse->tx_id, pResponse->tx_type); 426 | } 427 | break; 428 | 429 | default: 430 | ESP_LOGD(TAG, "Received frame from unknown device in unknown state; type 0x%02X from ID 0x%02X type 0x%02X", 431 | pResponse->command, pResponse->tx_id, pResponse->tx_type); 432 | break; 433 | } 434 | } 435 | 436 | static uint8_t minmax(const uint8_t value, const uint8_t min, const uint8_t max) { 437 | if (value <= min) { 438 | return min; 439 | } else if (value >= max) { 440 | return max; 441 | } else { 442 | return value; 443 | } 444 | } 445 | 446 | uint8_t ZehnderRF::createDeviceID(void) { 447 | uint8_t random = (uint8_t) random_uint32(); 448 | // Generate random device_id; don't use 0x00 and 0xFF 449 | 450 | // TODO: there's a 1 in 255 chance that the generated ID matches the ID of the main unit. Decide how to deal 451 | // withthis (some sort of ping discovery?) 452 | 453 | return minmax(random, 1, 0xFE); 454 | } 455 | 456 | void ZehnderRF::queryDevice(void) { 457 | RfFrame *const pFrame = (RfFrame *) this->_txFrame; // frame helper 458 | 459 | ESP_LOGD(TAG, "Query device"); 460 | 461 | this->lastFanQuery_ = millis(); // Update time 462 | 463 | // Clear frame data 464 | (void) memset(this->_txFrame, 0, FAN_FRAMESIZE); 465 | 466 | // Build frame 467 | pFrame->rx_type = this->config_.fan_main_unit_type; 468 | pFrame->rx_id = this->config_.fan_main_unit_id; 469 | pFrame->tx_type = this->config_.fan_my_device_type; 470 | pFrame->tx_id = this->config_.fan_my_device_id; 471 | pFrame->ttl = FAN_TTL; 472 | pFrame->command = FAN_TYPE_QUERY_DEVICE; 473 | pFrame->parameter_count = 0x00; // No parameters 474 | 475 | this->startTransmit(this->_txFrame, FAN_TX_RETRIES, [this]() { 476 | ESP_LOGW(TAG, "Query Timeout"); 477 | this->state_ = StateIdle; 478 | }); 479 | 480 | this->state_ = StateWaitQueryResponse; 481 | } 482 | 483 | void ZehnderRF::setSpeed(const uint8_t paramSpeed, const uint8_t paramTimer) { 484 | RfFrame *const pFrame = (RfFrame *) this->_txFrame; // frame helper 485 | uint8_t speed = paramSpeed; 486 | uint8_t timer = paramTimer; 487 | 488 | if (speed > this->speed_count_) { 489 | ESP_LOGW(TAG, "Requested speed too high (%u)", speed); 490 | speed = this->speed_count_; 491 | } 492 | 493 | ESP_LOGD(TAG, "Set speed: 0x%02X; Timer %u minutes", speed, timer); 494 | 495 | if (this->state_ == StateIdle) { 496 | (void) memset(this->_txFrame, 0, FAN_FRAMESIZE); // Clear frame data 497 | 498 | // Build frame 499 | pFrame->rx_type = this->config_.fan_main_unit_type; 500 | pFrame->rx_id = this->config_.fan_main_unit_id; 501 | pFrame->tx_type = this->config_.fan_my_device_type; 502 | pFrame->tx_id = this->config_.fan_my_device_id; 503 | pFrame->ttl = FAN_TTL; 504 | 505 | if (timer == 0) { 506 | pFrame->command = FAN_FRAME_SETSPEED; 507 | pFrame->parameter_count = sizeof(RfPayloadFanSetSpeed); 508 | pFrame->payload.setSpeed.speed = speed; 509 | } else { 510 | pFrame->command = FAN_FRAME_SETTIMER; 511 | pFrame->parameter_count = sizeof(RfPayloadFanSetTimer); 512 | pFrame->payload.setTimer.speed = speed; 513 | pFrame->payload.setTimer.timer = timer; 514 | } 515 | 516 | this->startTransmit(this->_txFrame, FAN_TX_RETRIES, [this]() { 517 | ESP_LOGW(TAG, "Set speed timeout"); 518 | this->state_ = StateIdle; 519 | }); 520 | 521 | newSetting = false; 522 | this->state_ = StateWaitSetSpeedResponse; 523 | } else { 524 | ESP_LOGD(TAG, "Invalid state, I'm trying later again"); 525 | newSpeed = speed; 526 | newTimer = timer; 527 | newSetting = true; 528 | } 529 | } 530 | 531 | void ZehnderRF::discoveryStart(const uint8_t deviceId) { 532 | RfFrame *const pFrame = (RfFrame *) this->_txFrame; // frame helper 533 | nrf905::Config rfConfig; 534 | 535 | ESP_LOGD(TAG, "Start discovery with ID %u", deviceId); 536 | 537 | this->config_.fan_my_device_type = FAN_TYPE_REMOTE_CONTROL; 538 | this->config_.fan_my_device_id = deviceId; 539 | 540 | // Build frame 541 | (void) memset(this->_txFrame, 0, FAN_FRAMESIZE); // Clear frame data 542 | 543 | // Set payload, available for linking 544 | pFrame->rx_type = 0x04; 545 | pFrame->rx_id = 0x00; 546 | pFrame->tx_type = this->config_.fan_my_device_type; 547 | pFrame->tx_id = this->config_.fan_my_device_id; 548 | pFrame->ttl = FAN_TTL; 549 | pFrame->command = FAN_NETWORK_JOIN_ACK; 550 | pFrame->parameter_count = sizeof(RfPayloadNetworkJoinAck); 551 | pFrame->payload.networkJoinAck.networkId = NETWORK_LINK_ID; 552 | 553 | // Set RX and TX address 554 | rfConfig = this->rf_->getConfig(); 555 | rfConfig.rx_address = NETWORK_LINK_ID; 556 | this->rf_->updateConfig(&rfConfig, NULL); 557 | this->rf_->writeTxAddress(NETWORK_LINK_ID, NULL); 558 | 559 | this->startTransmit(this->_txFrame, FAN_TX_RETRIES, [this]() { 560 | ESP_LOGW(TAG, "Start discovery timeout"); 561 | this->state_ = StateStartDiscovery; 562 | }); 563 | 564 | // Update state 565 | this->state_ = StateDiscoveryWaitForLinkRequest; 566 | } 567 | 568 | Result ZehnderRF::startTransmit(const uint8_t *const pData, const int8_t rxRetries, 569 | const std::function callback) { 570 | Result result = ResultOk; 571 | unsigned long startTime; 572 | bool busy = true; 573 | 574 | if (this->rfState_ != RfStateIdle) { 575 | ESP_LOGW(TAG, "TX still ongoing"); 576 | result = ResultBusy; 577 | } else { 578 | this->onReceiveTimeout_ = callback; 579 | this->retries_ = rxRetries; 580 | 581 | // Write data to RF 582 | // if (pData != NULL) { // If frame given, load it in the nRF. Else use previous TX payload 583 | // ESP_LOGD(TAG, "Write payload"); 584 | this->rf_->writeTxPayload(pData, FAN_FRAMESIZE); // Use framesize 585 | // } 586 | 587 | this->rfState_ = RfStateWaitAirwayFree; 588 | this->airwayFreeWaitTime_ = millis(); 589 | } 590 | 591 | return result; 592 | } 593 | 594 | void ZehnderRF::rfComplete(void) { 595 | this->retries_ = -1; // Disable this->retries_ 596 | this->rfState_ = RfStateIdle; 597 | } 598 | 599 | void ZehnderRF::rfHandler(void) { 600 | switch (this->rfState_) { 601 | case RfStateIdle: 602 | break; 603 | 604 | case RfStateWaitAirwayFree: 605 | if ((millis() - this->airwayFreeWaitTime_) > 5000) { 606 | ESP_LOGW(TAG, "Airway too busy, giving up"); 607 | this->rfState_ = RfStateIdle; 608 | 609 | if (this->onReceiveTimeout_ != NULL) { 610 | this->onReceiveTimeout_(); 611 | } 612 | } else if (this->rf_->airwayBusy() == false) { 613 | ESP_LOGD(TAG, "Start TX"); 614 | this->rf_->startTx(FAN_TX_FRAMES, nrf905::Receive); // After transmit, wait for response 615 | 616 | this->rfState_ = RfStateTxBusy; 617 | } 618 | break; 619 | 620 | case RfStateTxBusy: 621 | break; 622 | 623 | case RfStateRxWait: 624 | if ((this->retries_ >= 0) && ((millis() - this->msgSendTime_) > FAN_REPLY_TIMEOUT)) { 625 | ESP_LOGD(TAG, "Receive timeout"); 626 | 627 | if (this->retries_ > 0) { 628 | --this->retries_; 629 | ESP_LOGD(TAG, "No data received, retry again (left: %u)", this->retries_); 630 | 631 | this->rfState_ = RfStateWaitAirwayFree; 632 | this->airwayFreeWaitTime_ = millis(); 633 | } else if (this->retries_ == 0) { 634 | // Oh oh, ran out of options 635 | 636 | ESP_LOGD(TAG, "No messages received, giving up now..."); 637 | if (this->onReceiveTimeout_ != NULL) { 638 | this->onReceiveTimeout_(); 639 | } 640 | 641 | // Back to idle 642 | this->rfState_ = RfStateIdle; 643 | } 644 | } 645 | break; 646 | 647 | default: 648 | break; 649 | } 650 | } 651 | 652 | } // namespace zehnder 653 | } // namespace esphome 654 | --------------------------------------------------------------------------------