diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dea8167 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 wrfz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/daikin_rotex_can/Accessor.h b/components/daikin_rotex_can/Accessor.h new file mode 100644 index 0000000..dfb46f4 --- /dev/null +++ b/components/daikin_rotex_can/Accessor.h @@ -0,0 +1,106 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text/text.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/daikin_rotex_can/BidiMap.h" +#include "esphome/components/daikin_rotex_can/utils.h" +#include + +namespace esphome { +namespace daikin_rotex_can { + +class DaikinRotexCanComponent; + +class Accessor { + using THandleFunc = std::function; + using TSetFunc = std::function; + + struct TEntityArguments { + EntityBase* pEntity; + std::string id; + TMessage command; + uint8_t data_offset; + uint8_t data_size; + float divider; + BidiMap map; + std::string update_entity; + uint16_t update_interval; + THandleFunc handle_lambda; + TSetFunc set_lambda; + bool handle_lambda_set; + bool set_lambda_set; + + TEntityArguments( + EntityBase* _pEntity, + std::string const& _id, + std::string const& _command, + uint8_t _data_offset, + uint8_t _data_size, + float _divider, + std::string const& _map, + std::string const& _update_entity, + uint16_t _update_interval, + THandleFunc _handle_lambda, + TSetFunc _set_lambda, + bool _handle_lambda_set, + bool _set_lambda_set + ) + : pEntity(_pEntity) + , id(_id) + , command(Utils::str_to_bytes_array8(_command)) + , data_offset(_data_offset) + , data_size(_data_size) + , divider(_divider) + , map(Utils::str_to_map(_map)) + , update_entity(_update_entity) + , update_interval(_update_interval) + , handle_lambda(_handle_lambda) + , set_lambda(_set_lambda) + , handle_lambda_set(_handle_lambda_set) + , set_lambda_set(_set_lambda_set) + {} + }; + using TEntityArgumentsList = std::list; +public: + Accessor(DaikinRotexCanComponent* pDaikinRotexCanComponent) + : m_log_filter(nullptr) + , m_custom_request_text(nullptr) + , m_thermal_power(nullptr) + , m_pDaikinRotexCanComponent(pDaikinRotexCanComponent) { + } + + DaikinRotexCanComponent* getDaikinRotexCanComponent() const { + return m_pDaikinRotexCanComponent; + } + + TEntityArgumentsList const& get_entities() const { return m_entities; } + void set_entity(std::string const& name, TEntityArguments const& arg) { m_entities.push_back(arg); } + + // Texts + + text::Text* get_log_filter() const { return m_log_filter; } + void set_log_filter(text::Text* pText) { m_log_filter = pText; } + + text::Text* get_custom_request_text() const { return m_custom_request_text; } + void set_custom_request_text(text::Text* pText) { m_custom_request_text = pText; } + + // Sensors + + sensor::Sensor* get_thermal_power() const { return m_thermal_power; } + void set_thermal_power(sensor::Sensor* pSensor) { m_thermal_power = pSensor; } + +private: + text::Text* m_log_filter; + text::Text* m_custom_request_text; + + TEntityArgumentsList m_entities; + + // Sensors + sensor::Sensor* m_thermal_power; + + DaikinRotexCanComponent* m_pDaikinRotexCanComponent; +}; + +} +} \ No newline at end of file diff --git a/components/daikin_rotex_can/BidiMap.h b/components/daikin_rotex_can/BidiMap.h new file mode 100644 index 0000000..5b47651 --- /dev/null +++ b/components/daikin_rotex_can/BidiMap.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +template +class BidiMap { +public: + using Iterator = typename std::map::const_iterator; + + BidiMap(std::initializer_list> init_list) + : key_to_value(init_list) { + for (const auto& pair : init_list) { + value_to_key[pair.second] = pair.first; + } + } + + BidiMap(const std::map& map) + : key_to_value(map) { + for (const auto& pair : map) { + value_to_key[pair.second] = pair.first; + } + } + + Iterator findByKey(const KeyType& key) const { + return key_to_value.find(key); + } + + Iterator findByValue(const ValueType& value) const { + auto it = value_to_key.find(value); + if (it != value_to_key.end()) { + return key_to_value.find(it->second); + } + return key_to_value.end(); + } + + Iterator end() const { + return key_to_value.end(); + } + + std::string string() const { + std::stringstream ss; + for (const auto& pair : key_to_value) { + ss << "{" << std::to_string(pair.first) << ", " << pair.second << "} "; + } + return ss.str(); + } + +private: + std::map key_to_value; + std::map value_to_key; +}; diff --git a/components/daikin_rotex_can/__init__.py b/components/daikin_rotex_can/__init__.py new file mode 100644 index 0000000..9b1dccc --- /dev/null +++ b/components/daikin_rotex_can/__init__.py @@ -0,0 +1,1155 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, button, number, select, text_sensor, canbus, text +from esphome.const import * +from esphome.core import Lambda +from esphome.cpp_generator import MockObj +from esphome.cpp_types import std_ns +from esphome.components.canbus import CanbusComponent +#from esphome.const import ( +# ENTITY_CATEGORY_CONFIG, +#) + +daikin_rotex_can_ns = cg.esphome_ns.namespace('daikin_rotex_can') +DaikinRotexCanComponent = daikin_rotex_can_ns.class_('DaikinRotexCanComponent', cg.Component) + +GenericSelect = daikin_rotex_can_ns.class_("GenericSelect", select.Select) +GenericNumber = daikin_rotex_can_ns.class_("GenericNumber", number.Number) + +LogFilterText = daikin_rotex_can_ns.class_("LogFilterText", text.Text) +CustomRequestText = daikin_rotex_can_ns.class_("CustomRequestText", text.Text) + +DHWRunButton = daikin_rotex_can_ns.class_("DHWRunButton", button.Button) +DumpButton = daikin_rotex_can_ns.class_("DumpButton", button.Button) + +UNIT_BAR = "bar" +UNIT_LITER_PER_HOUR = "L/h" + +########## Icons ########## +ICON_SUN_SNOWFLAKE_VARIANT = "mdi:sun-snowflake-variant" + +########## Configuration of Sensors, TextSensors, BinarySensors, Selects and Numbers ########## + +sensor_configuration = [ + { + "type": "select", + "name": "outdoor_unit" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 06 9A 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "--", + 0x01: "4", + 0x02: "6", + 0x03: "8", + 0x04: "11", + 0x05: "14", + 0x06: "16" + } + }, + { + "type": "select", + "name": "indoor_unit" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 06 99 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "--", + 0x01: "304", + 0x02: "308", + 0x03: "508", + 0x04: "516" + } + }, + { + "type": "number", + "name": "antileg_temp", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 60, + "max_value": 75, + "step": 1, + "command": "31 00 FA 05 87 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "select", + "name": "antileg_day" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 01 01 00 00", + "data_offset": 5, + "data_size": 1, + "map": { + 0x00: "Aus", + 0x01: "Mo", + 0x02: "Di", + 0x03: "Mi", + 0x04: "Do", + 0x05: "Fr", + 0x06: "Sa", + 0x07: "So", + 0x08: "Mo-So" + } + }, + { + "type": "number", + "name": "circulation_interval_on", + "unit_of_measurement": UNIT_MINUTE, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 0, + "max_value": 15, + "step": 1, + "command": "31 00 FA 06 5E 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "number", + "name": "circulation_interval_off", + "unit_of_measurement": UNIT_MINUTE, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 0, + "max_value": 15, + "step": 1, + "command": "31 00 FA 06 5F 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "select", + "name": "circulation_with_dhw_program" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 01 82 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Aus", + 0x01: "An" + } + }, + { + "type": "number", + "name": "t_dhw_1_min", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 20, + "max_value": 85, + "step": 1, + "command": "31 00 FA 06 73 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "number", + "name": "max_dhw_loading", + "unit_of_measurement": UNIT_MINUTE, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 10, + "max_value": 240, + "step": 1, + "command": "31 00 FA 01 80 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "number", + "name": "dhw_off_time", + "unit_of_measurement": UNIT_MINUTE, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 0, + "max_value": 180, + "step": 1, + "command": "31 00 FA 4E 3F 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "number", + "name": "tdiff_dhw_ch", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_KELVIN, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 2, + "max_value": 15, + "step": 1, + "command": "31 00 FA 06 6D 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "t_hs", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "command": "31 00 FA 01 D6 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "temperature_outside", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "command": "31 00 FA C0 FF 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "t_ext", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "command": "61 00 FA 0A 0C 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "tdhw1", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "command": "31 00 FA 00 0E 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "water_pressure", + "device_class": DEVICE_CLASS_PRESSURE, + "unit_of_measurement": UNIT_BAR, + "accuracy_decimals": 2, + "state_class": STATE_CLASS_MEASUREMENT, + "command": "31 00 1C 00 00 00 00", + "data_offset": 3, + "data_size": 2, + "divider": 1000.0 + }, + { + "type": "sensor", + "name": "circulation_pump", + "device_class": DEVICE_CLASS_VOLUME_FLOW_RATE, + "unit_of_measurement": UNIT_PERCENT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:pump", + "command": "31 00 FA C0 F7 00 00", + "data_offset": 6, + "data_size": 1, + "divider": 1 + }, + { + "type": "number", + "name": "circulation_pump_min", + "device_class": DEVICE_CLASS_VOLUME_FLOW_RATE, + "unit_of_measurement": UNIT_PERCENT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "min_value": 40, + "max_value": 100, + "step": 1, + "command": "31 00 FA 06 7F 00 00", + "data_offset": 6, + "data_size": 1, + "divider": 1 + }, + { + "type": "number", + "name": "circulation_pump_max", + "device_class": DEVICE_CLASS_VOLUME_FLOW_RATE, + "unit_of_measurement": UNIT_PERCENT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-right", + "min_value": 60, + "max_value": 100, + "step": 1, + "command": "31 00 FA 06 7E 00 00", + "data_offset": 6, + "data_size": 1, + "divider": 1 + }, + { + "type": "sensor", + "name": "bypass_valve", + "device_class": DEVICE_CLASS_VOLUME_FLOW_RATE, + "unit_of_measurement": UNIT_PERCENT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "command": "31 00 FA C0 FB 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "dhw_mixer_position", + "device_class": DEVICE_CLASS_VOLUME_FLOW_RATE, + "unit_of_measurement": UNIT_PERCENT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "command": "31 00 FA 06 9B 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "target_supply_temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "command": "31 00 02 00 00 00 00", + "data_offset": 3, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "ehs_for_ch", + "device_class": DEVICE_CLASS_ENERGY_STORAGE, + "unit_of_measurement": UNIT_KILOWATT_HOURS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:transmission-tower", + "command": "31 00 FA 09 20 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "qch", + "device_class": DEVICE_CLASS_ENERGY_STORAGE, + "unit_of_measurement": UNIT_KILOWATT_HOURS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:transmission-tower", + "command": "31 00 FA 06 A7 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "qboh", + "device_class": DEVICE_CLASS_ENERGY_STORAGE, + "unit_of_measurement": UNIT_KILOWATT_HOURS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:transmission-tower", + "command": "31 00 FA 09 1C 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "qdhw", + "device_class": DEVICE_CLASS_ENERGY_STORAGE, + "unit_of_measurement": UNIT_KILOWATT_HOURS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:transmission-tower", + "command": "31 00 FA 09 2C 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "total_energy_produced", + "device_class": DEVICE_CLASS_ENERGY_STORAGE, + "unit_of_measurement": UNIT_KILOWATT_HOURS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:transmission-tower", + "command": "31 00 FA 09 30 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "runtime_compressor", + "unit_of_measurement": UNIT_HOUR, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:clock-time-two-outline", + "command": "31 00 FA 06 A5 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "runtime_pump", + "unit_of_measurement": UNIT_HOUR, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:clock-time-two-outline", + "command": "31 00 FA 06 A4 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1 + }, + { + "type": "sensor", + "name": "delta_temp_ch", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_KELVIN, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:thermometer-lines", + "command": "31 00 FA 06 83 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "delta_temp_dhw", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_KELVIN, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:thermometer-lines", + "command": "31 00 FA 06 84 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "sensor", + "name": "tv", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:thermometer-lines", + "command": "31 00 FA C0 FC 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0, + "update_entity": "thermal_power" + }, + { + "type": "sensor", + "name": "tvbh", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:thermometer-lines", + "command": "31 00 FA C1 02 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0, + "update_entity": "thermal_power" + }, + { + "type": "sensor", + "name": "tr", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:thermometer-lines", + "command": "31 00 FA C1 00 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0, + "update_entity": "thermal_power" + }, + { + "type": "sensor", + "name": "flow_rate", + "device_class": DEVICE_CLASS_WATER, + "unit_of_measurement": UNIT_LITER_PER_HOUR, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:thermometer-lines", + "command": "31 00 FA 01 DA 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 1, + "update_entity": "thermal_power" + }, + { + "type": "number", + "name": "target_room1_temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 15, + "max_value": 25, + "step": 0.1, + "command": "31 00 05 00 00 00 00", + "data_offset": 3, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "number", + "name": "flow_temperature_day", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 25, + "max_value": 60, + "step": 1, + "command": "31 00 FA 01 29 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "number", + "name": "heating_curve", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 2, + "state_class": STATE_CLASS_MEASUREMENT, + "min_value": 0, + "max_value": 2.55, + "step": 0.01, + "command": "31 00 FA 01 0E 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 100.0 + }, + { + "type": "number", + "name": "min_target_flow_temp", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "min_value": 25, + "max_value": 40, + "step": 1, + "command": "31 00 FA 01 2B 00 00", + "data_offset": 5, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "number", + "name": "max_target_flow_temp", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-right", + "min_value": 25, + "max_value": 60, + "step": 1, + "command": "31 00 28 00 00 00 00", + "data_offset": 3, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "number", + "name": "target_hot_water_temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + "unit_of_measurement": UNIT_CELSIUS, + "accuracy_decimals": 1, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-right", + "min_value": 35, + "max_value": 70, + "step": 1, + "command": "31 00 13 00 00 00 00", + "data_offset": 3, + "data_size": 2, + "divider": 10.0 + }, + { + "type": "text_sensor", + "name": "mode_of_operating" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA C0 F6 00 00", + "data_offset": 5, + "data_size": 2, + "map": { + 0x00: "Standby", + 0x01: "Heizen", + 0x02: "Kühlen", + 0x03: "Abtauen", + 0x04: "Warmwasserbereitung" + }, + "update_entity": "thermal_power" + }, + { + "type": "text_sensor", + "name": "quiet" , + "icon": "mdi:weather-partly-cloudy", + "command": "31 00 FA 06 96", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Aus", + 0x01: "An", + 0x02: "Nur bei Nacht" + } + }, + { + "type": "text_sensor", + "name": "error_code" , + "icon": "mdi:alert", + "command": "31 00 FA 13 88 00 00", + "data_offset": 5, + "data_size": 2, + "map": { + 0: "Kein Fehler", + 9001: "E9001 Rücklauffühler", + 9002: "E9002 Vorlauffühler", + 9003: "E9003 Frostschutzfunktion", + 9004: "E9004 Durchfluss", + 9005: "E9005 Vorlauftemperaturfühler", + 9006: "E9006 Vorlauftemperaturfühler", + 9007: "E9007 Platine IG defekt", + 9008: "E9008 Kältemitteltemperatur außerhalb des Bereiches", + 9009: "E9009 STB Fehler", + 9010: "E9010 STB Fehler", + 9011: "E9011 Fehler Flowsensor", + 9012: "E9012 Fehler Vorlauffühler", + 9013: "E9013 Platine AG defekt", + 9014: "E9014 P-Kältemittel hoch", + 9015: "E9015 P-Kältemittel niedrig", + 9016: "E9016 Lastschutz Verdichter", + 9017: "E9017 Ventilator blockiert", + 9018: "E9018 Expansionsventil", + 9019: "E9019 Warmwassertemperatur > 85°C", + 9020: "E9020 T-Verdampfer hoch", + 9021: "E9021 HPS-System", + 9022: "E9022 Fehler AT-Fühler", + 9023: "E9023 Fehler WW-Fühler", + 9024: "E9024 Drucksensor", + 9025: "E9025 Fehler Rücklauffühler", + 9026: "E9026 Drucksensor", + 9027: "E9027 Aircoil-Fühler Defrost", + 9028: "E9028 Aircoil-Fühler temp", + 9029: "E9029 Fehler Kältefühler AG", + 9030: "E9030 Defekt elektrisch", + 9031: "E9031 Defekt elektrisch", + 9032: "E9032 Defekt elektrisch", + 9033: "E9033 Defekt elektrisch", + 9034: "E9034 Defekt elektrisch", + 9035: "E9035 Platine AG defekt", + 9036: "E9036 Defekt elektrisch", + 9037: "E9037 Einstellung Leistung", + 9038: "E9038 Kältemittel Leck", + 9039: "E9039 Unter/Überspannung", + 9041: "E9041 Übertragungsfehler", + 9042: "E9042 Übertragungsfehler", + 9043: "E9043 Übertragungsfehler", + 9044: "E9044 Übertragungsfehler", + 75: "E75 Fehler Außentemperaturfühler", + 76: "E76 Fehler Speichertemperaturfühler", + 81: "E81 Kommunikationsfehler Rocon", + 88: "E88 Kommunikationsfehler Rocon Handbuch", + 91: "E91 Kommunikationsfehler Rocon Handbuch", + 128: "E128 Fehler Rücklauftemperaturfühler", + 129: "E129 Fehler Drucksensor", + 198: "E198 Durchflussmessung nicht plausibel", + 200: "E200 Kommunikationsfehler", + 8005: "E8005 Wasserdruck in Heizungsanlage zu gering", + 8100: "E8100 Kommunikation", + 9000: "E9000 Interne vorübergehende Meldung", + 8006: "W8006 Warnung Druckverlust", + 8007: "W8007 Wasserdruck in Anlage zu hoch" + } + }, + { + "type": "binary_sensor", + "name": "status_kompressor" , + "icon": "mdi:pump", + "command": "A1 00 61 00 00 00 00", + "data_offset": 3, + "data_size": 1 + }, + { + "type": "binary_sensor", + "name": "status_kesselpumpe" , + "icon": "mdi:pump", + "command": "31 00 FA 0A 8C 00 00", + "data_offset": 6, + "data_size": 1 + }, + { + "type": "select", + "name": "operating_mode" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 01 12 00 00", + "data_offset": 5, + "data_size": 1, + "map": { + 0x01: "Bereitschaft", + 0x03: "Heizen", + 0x04: "Absenken", + 0x05: "Sommer", + 0x11: "Kühlen", + 0x0B: "Automatik 1", + 0x0C: "Automatik 2" + } + }, + { + "type": "select", + "name": "hk_function" , + "icon": "mdi:weather-partly-cloudy", + "command": "31 00 FA 01 41 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Witterungsgeführt", + 0x01: "Fest" + } + }, + { + "type": "select", + "name": "sg_mode" , + "icon": "mdi:weather-partly-cloudy", + "command": "31 00 FA 06 94 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Aus", + 0x01: "SG Modus 1", + 0x02: "SG Modus 2" + } + }, + { + "type": "select", + "name": "smart_grid" , + "icon": "mdi:weather-partly-cloudy", + "command": "31 00 FA 06 93 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Aus", + 0x01: "An" + } + }, + { + "type": "select", + "name": "function_ehs" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 06 D2 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Kein zusätzlicher Wärmeerzeuger", + 0x01: "Optionaler Backup-Heater", + 0x02: "WEZ für WW und HZ", + 0x03: "WEZ1 für WW - WEZ2 für HZ" + } + }, + { + "type": "select", + "name": "ch_support" , + "icon": ICON_SUN_SNOWFLAKE_VARIANT, + "command": "31 00 FA 06 6C 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x00: "Aus", + 0x01: "An" + } + }, + { + "type": "number", + "name": "power_dhw", + "device_class": DEVICE_CLASS_POWER, + "unit_of_measurement": UNIT_KILOWATT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "min_value": 1, + "max_value": 40, + "step": 1, + "command": "31 00 FA 06 68 00 00", + "handle_lambda": """ + return ((data[5] << 8) | data[6]) / 0x64; + """, + "set_lambda": """ + const uint16_t u16val = value * 0x64; + data[5] = (u16val >> 8) & 0xFF; + data[6] = u16val & 0xFF; + """ + }, + { + "type": "number", + "name": "power_ehs_1", + "device_class": DEVICE_CLASS_POWER, + "unit_of_measurement": UNIT_KILOWATT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "min_value": 1, + "max_value": 40, + "step": 1, + "command": "31 00 FA 06 69 00 00", + "handle_lambda": """ + return ((data[5] << 8) | data[6]) / 0x64; + """, + "set_lambda": """ + const uint16_t u16val = value * 0x64; + data[5] = (u16val >> 8) & 0xFF; + data[6] = u16val & 0xFF; + """ + }, + { + "type": "number", + "name": "power_ehs_2", + "device_class": DEVICE_CLASS_POWER, + "unit_of_measurement": UNIT_KILOWATT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "min_value": 1, + "max_value": 40, + "step": 1, + "command": "31 00 FA 06 6A 00 00", + "handle_lambda": """ + return ((data[5] << 8) | data[6]) / 0x64; + """, + "set_lambda": """ + const uint16_t u16val = value * 0x64; + data[5] = (u16val >> 8) & 0xFF; + data[6] = u16val & 0xFF; + """ + }, + { + "type": "number", + "name": "power_biv", + "device_class": DEVICE_CLASS_POWER, + "unit_of_measurement": UNIT_KILOWATT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:waves-arrow-left", + "min_value": 3, + "max_value": 40, + "step": 1, + "command": "31 00 FA 06 6B 00 00", + "handle_lambda": """ + return ((data[5] << 8) | data[6]) / 0x64; + """, + "set_lambda": """ + const uint16_t u16val = value * 0x64; + data[5] = (u16val >> 8) & 0xFF; + data[6] = u16val & 0xFF; + """ + }, + { + "type": "select", + "name": "electric_heater", + "device_class": DEVICE_CLASS_POWER, + "unit_of_measurement": UNIT_KILOWATT, + "accuracy_decimals": 0, + "state_class": STATE_CLASS_MEASUREMENT, + "icon": "mdi:induction", + "command": "31 00 FA 0A 20 00 00", + "data_offset": 5, + "data_size": 2, + "map": { + 0x00: "Aus", + 0x03: "3 kW", + 0x06: "6 kW", + 0x09: "9 kW" + }, + "handle_lambda": """ + return + bool(data[5] & 0b00001000) * 3 + + bool(data[5] & 0b00000100) * 3 + + bool(data[5] & 0b00000010) * 3; + """, + "set_lambda": """ + data[5] = 0b00000001; + if (value >= 3) data[5] |= 0b00001000; + if (value >= 6) data[5] |= 0b00000100; + if (value >= 9) data[5] |= 0b00000010; + """ + }, + { + "type": "text_sensor", + "name": "ext", + "accuracy_decimals": 0, + "icon": "mdi:transmission-tower-import", + "command": "31 00 FA C0 F8 00 00", + "data_offset": 6, + "data_size": 1, + "map": { + 0x03: "SGN - Normaler Modus", + 0x04: "SG1 - WW & HZ ausgeschalten", + 0x05: "SG2 - WW & HZ + 5°C", + 0x06: "SG3 - WW 70°C" + } + } +] + +DEPENDENCIES = [] + +AUTO_LOAD = ['binary_sensor', 'button', 'number', 'sensor', 'select', 'text', 'text_sensor'] + +CONF_CAN_ID = "canbus_id" +CONF_UPDATE_INTERVAL = "update_interval" +CONF_LOG_FILTER_TEXT = "log_filter" +CONF_CUSTOM_REQUEST_TEXT = "custom_request" +CONF_ENTITIES = "entities" + +########## Sensors ########## + +CONF_THERMAL_POWER = "thermal_power" # Thermische Leistung + +CONF_DUMP = "dump" +CONF_DHW_RUN = "dhw_run" + +DEFAULT_UPDATE_INTERVAL = 30 # seconds + +entity_schemas = {} +for sensor_conf in sensor_configuration: + match sensor_conf.get("type"): + case "sensor": + entity_schemas.update({ + cv.Optional(sensor_conf.get("name")): sensor.sensor_schema( + device_class=(sensor_conf.get("device_class") if sensor_conf.get("device_class") != None else sensor._UNDEF), + unit_of_measurement=sensor_conf.get("unit_of_measurement"), + accuracy_decimals=sensor_conf.get("accuracy_decimals"), + state_class=sensor_conf.get("state_class"), + icon=(sensor_conf.get("icon") if sensor_conf.get("icon") != None else sensor._UNDEF) + ).extend({cv.Optional(CONF_UPDATE_INTERVAL): cv.uint16_t}), + }) + case "text_sensor": + entity_schemas.update({ + cv.Optional(sensor_conf.get("name")): text_sensor.text_sensor_schema( + icon=sensor_conf.get("icon") + ).extend({cv.Optional(CONF_UPDATE_INTERVAL): cv.uint16_t}), + }) + case "binary_sensor": + entity_schemas.update({ + cv.Optional(sensor_conf.get("name")): binary_sensor.binary_sensor_schema( + icon=(sensor_conf.get("icon") if sensor_conf.get("icon") != None else sensor._UNDEF) + ).extend({cv.Optional(CONF_UPDATE_INTERVAL): cv.uint16_t}), + }) + case "select": + entity_schemas.update({ + cv.Optional(sensor_conf.get("name")): select.select_schema( + GenericSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=sensor_conf.get("icon") + ).extend({cv.Optional(CONF_UPDATE_INTERVAL): cv.uint16_t}), + }) + case "number": + entity_schemas.update({ + cv.Optional(sensor_conf.get("name")): number.number_schema( + GenericNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SUN_SNOWFLAKE_VARIANT + ).extend({ + cv.Optional(CONF_UPDATE_INTERVAL): cv.uint16_t, + cv.Optional(CONF_MODE, default="BOX"): cv.enum(number.NUMBER_MODES, upper=True) + }) + }) + +entity_schemas.update({ + ########## Sensors ########## + + cv.Optional(CONF_THERMAL_POWER): sensor.sensor_schema( + device_class=DEVICE_CLASS_POWER, + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT + ).extend(), + + ########## Buttons ########## + + cv.Optional(CONF_DHW_RUN): button.button_schema( + DHWRunButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SUN_SNOWFLAKE_VARIANT + ).extend(), +}) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(DaikinRotexCanComponent), + cv.Required(CONF_CAN_ID): cv.use_id(CanbusComponent), + cv.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): cv.uint16_t, + + ########## Texts ########## + + cv.Optional(CONF_LOG_FILTER_TEXT): text.TEXT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(LogFilterText), + cv.Optional(CONF_MODE, default="TEXT"): cv.enum(text.TEXT_MODES, upper=True), + } + ), + cv.Optional(CONF_CUSTOM_REQUEST_TEXT): text.TEXT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CustomRequestText), + cv.Optional(CONF_MODE, default="TEXT"): cv.enum(text.TEXT_MODES, upper=True), + } + ), + cv.Optional(CONF_DUMP): button.button_schema( + DumpButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_SUN_SNOWFLAKE_VARIANT + ).extend(), + + cv.Required(CONF_ENTITIES): cv.Schema( + entity_schemas + ), + } +).extend(cv.COMPONENT_SCHEMA) + +async def to_code(config): + global_ns = MockObj("", "") + std_array_u8_7_const_ref = std_ns.class_("array const&") + std_array_u8_7_ref = std_ns.class_("array&") + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + if CONF_CAN_ID in config: + cg.add_define("USE_CANBUS") + canbus = await cg.get_variable(config[CONF_CAN_ID]) + cg.add(var.set_canbus(canbus)) + + ########## Texts ########## + + if text_conf := config.get(CONF_LOG_FILTER_TEXT): + t = await text.new_text(text_conf) + cg.add(var.getAccessor().set_log_filter(t)) + + if text_conf := config.get(CONF_CUSTOM_REQUEST_TEXT): + t = await text.new_text(text_conf) + await cg.register_parented(t, var) + cg.add(var.getAccessor().set_custom_request_text(t)) + + if button_conf := config.get(CONF_DUMP): + but = await button.new_button(button_conf) + await cg.register_parented(but, var) + + if entities := config.get(CONF_ENTITIES): + for sens_conf in sensor_configuration: + if yaml_sensor_conf := entities.get(sens_conf.get("name")): + entity = None + match sens_conf.get("type"): + case "sensor": + entity = await sensor.new_sensor(yaml_sensor_conf) + case "text_sensor": + entity = await text_sensor.new_text_sensor(yaml_sensor_conf) + case "binary_sensor": + entity = await binary_sensor.new_binary_sensor(yaml_sensor_conf) + case "select": + entity = await select.new_select(yaml_sensor_conf, options = list(sens_conf.get("map").values())) + cg.add(entity.set_id(sens_conf.get("name"))) + await cg.register_parented(entity, var) + case "number": + if "min_value" not in sens_conf: + raise Exception("min_value is required for number: " + sens_conf.get("name")) + if "max_value" not in sens_conf: + raise Exception("max_value is required for number: " + sens_conf.get("name")) + if "step" not in sens_conf: + raise Exception("step is required for number: " + sens_conf.get("name")) + entity = await number.new_number( + yaml_sensor_conf, + min_value=sens_conf.get("min_value"), + max_value=sens_conf.get("max_value"), + step=sens_conf.get("step") + ) + cg.add(entity.set_id(sens_conf.get("name"))) + + await cg.register_parented(entity, var) + case _: + raise Exception("Unknown type: " + sens_conf.get("type")) + + update_interval = yaml_sensor_conf.get(CONF_UPDATE_INTERVAL, -1) + if update_interval < 0: + update_interval = config[CONF_UPDATE_INTERVAL] + + if "command" not in sens_conf: + raise Exception("command is required for number: " + sens_conf.get("name")) + + async def handle_lambda(): + lamb = str(sens_conf.get("handle_lambda")) if "handle_lambda" in sens_conf else "return 0;" + return await cg.process_lambda( + Lambda(lamb), + [(std_array_u8_7_const_ref, "data")], + return_type=cg.float_, + ) + + async def set_lambda(): + lamb = str(sens_conf.get("set_lambda")) if "set_lambda" in sens_conf else "" + return await cg.process_lambda( + Lambda(lamb), + [(std_array_u8_7_ref, "data"), (float, "value")], + return_type=cg.void, + ) + + cg.add(var.getAccessor().set_entity(sens_conf.get("name"), [ + entity, + sens_conf.get("name"), + sens_conf.get("command"), + sens_conf.get("data_offset", 5), + sens_conf.get("data_size", 1), + sens_conf.get("divider", 1.0), + "|".join([f"0x{key:02X}:{value}" for key, value in sens_conf.get("map", {}).items()]), # map + sens_conf.get("update_entity", ""), + update_interval, + await handle_lambda(), + await set_lambda(), + "handle_lambda" in sens_conf, + "set_lambda" in sens_conf + ])) + + ########## Sensors ########## + + if yaml_sensor_conf := entities.get(CONF_THERMAL_POWER): + sens = await sensor.new_sensor(yaml_sensor_conf) + cg.add(var.getAccessor().set_thermal_power(sens)) + + ########## Buttons ########## + + if button_conf := entities.get(CONF_DHW_RUN): + but = await button.new_button(button_conf) + await cg.register_parented(but, var) diff --git a/components/daikin_rotex_can/buttons.cpp b/components/daikin_rotex_can/buttons.cpp new file mode 100644 index 0000000..728fe83 --- /dev/null +++ b/components/daikin_rotex_can/buttons.cpp @@ -0,0 +1,15 @@ +#include "esphome/components/daikin_rotex_can/buttons.h" + +namespace esphome { +namespace daikin_rotex_can { + +void DHWRunButton::press_action() { + this->parent_->dhw_run(); +} + +void DumpButton::press_action() { + this->parent_->dump(); +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/components/daikin_rotex_can/buttons.h b/components/daikin_rotex_can/buttons.h new file mode 100644 index 0000000..db2d7c4 --- /dev/null +++ b/components/daikin_rotex_can/buttons.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/button/button.h" +#include "esphome/components/daikin_rotex_can/daikin_rotex_can.h" + +namespace esphome { +namespace daikin_rotex_can { + +class DHWRunButton : public button::Button, public Parented { +public: + DHWRunButton() = default; +protected: + void press_action() override; +}; + +class DumpButton : public button::Button, public Parented { +public: + DumpButton() = default; +protected: + void press_action() override; +}; + +} +} diff --git a/components/daikin_rotex_can/daikin_rotex_can.cpp b/components/daikin_rotex_can/daikin_rotex_can.cpp new file mode 100644 index 0000000..f35171f --- /dev/null +++ b/components/daikin_rotex_can/daikin_rotex_can.cpp @@ -0,0 +1,269 @@ +#include "esphome/components/daikin_rotex_can/daikin_rotex_can.h" +#include "esphome/components/daikin_rotex_can/request.h" +#include +#include + +namespace esphome { +namespace daikin_rotex_can { + +static const char* TAG = "daikin_rotex_can"; + +DaikinRotexCanComponent::DaikinRotexCanComponent() +: m_accessor(this) +, m_data_requests() +, m_later_calls() +, m_dhw_run_lambdas() +{ +} + +void DaikinRotexCanComponent::setup() { + ESP_LOGI(TAG, "setup"); + + for (auto const& entity_conf : m_accessor.get_entities()) { + + const uint32_t response_can_id = entity_conf.command.size() >= 7 ? (entity_conf.command[0] & 0xF0) * 8 + (entity_conf.command[1] & 0x0F) : 0x00; + if (response_can_id == 0x0) { + throwPeriodicError(Utils::format("Response can_id can't be calculated: %s", entity_conf.id.c_str())); + return; + } + + m_data_requests.add({ + entity_conf.id, + entity_conf.command, + entity_conf.pEntity, + [entity_conf, this](auto const& data) -> TRequest::TVariant { + TRequest::TVariant variant; + + if (entity_conf.data_offset > 0 && (entity_conf.data_offset + entity_conf.data_size) <= 7) { + if (entity_conf.data_size >= 1 && entity_conf.data_size <= 2) { + const float value = entity_conf.handle_lambda_set ? entity_conf.handle_lambda(data) : + ( + entity_conf.data_size == 2 ? + (((data[entity_conf.data_offset] << 8) + data[entity_conf.data_offset + 1]) / entity_conf.divider) : + (data[entity_conf.data_offset] / entity_conf.divider) + ); + + if (dynamic_cast(entity_conf.pEntity) != nullptr) { + Utils::toSensor(entity_conf.pEntity)->publish_state(value); + variant = value; + } else if (dynamic_cast(entity_conf.pEntity) != nullptr) { + Utils::toBinarySensor(entity_conf.pEntity)->publish_state(value); + variant = value; + } else if (dynamic_cast(entity_conf.pEntity) != nullptr) { + auto it = entity_conf.map.findByKey(value); + const std::string str = it != entity_conf.map.end() ? it->second : "INVALID"; + Utils::toTextSensor(entity_conf.pEntity)->publish_state(str); + variant = str; + } else if (dynamic_cast(entity_conf.pEntity) != nullptr) { + auto it = entity_conf.map.findByKey(value); + const std::string str = it != entity_conf.map.end() ? it->second : "INVALID"; + Utils::toSelect(entity_conf.pEntity)->publish_state(str); + variant = str; + } else if (dynamic_cast(entity_conf.pEntity) != nullptr) { + Utils::toNumber(entity_conf.pEntity)->publish_state(value); + variant = value; + } + } else { + call_later([entity_conf](){ + ESP_LOGE("validateConfig", "Invalid data size: %d", entity_conf.data_size); + }); + } + } else { + call_later([entity_conf](){ + ESP_LOGE("validateConfig", "Invalid data_offset: %d", entity_conf.data_offset); + }); + } + + if (!entity_conf.update_entity.empty()) { + call_later([entity_conf, this](){ + updateState(entity_conf.update_entity); + }); + } + + if (entity_conf.id == "target_hot_water_temperature") { + call_later([this](){ + run_dhw_lambdas(); + }); + } + + return variant; + }, + [&entity_conf](float const& value) -> TMessage { + TMessage message = TMessage(entity_conf.command); + message[0] = 0x30; + message[1] = 0x00; + if (entity_conf.set_lambda_set) { + entity_conf.set_lambda(message, value); + } else { + Utils::setBytes(message, value * entity_conf.divider, entity_conf.data_offset, entity_conf.data_size); + } + return message; + }, + entity_conf.update_interval + }); + } + + m_data_requests.removeInvalidRequests(); +} + +void DaikinRotexCanComponent::updateState(std::string const& id) { + if (id == "thermal_power") { + update_thermal_power(); + } +} + +void DaikinRotexCanComponent::update_thermal_power() { + text_sensor::TextSensor* mode_of_operating = m_data_requests.get_text_sensor("mode_of_operating"); + sensor::Sensor* thermal_power = m_accessor.get_thermal_power(); + + if (mode_of_operating != nullptr && thermal_power != nullptr) { + sensor::Sensor* flow_rate = m_data_requests.get_sensor("flow_rate"); + if (flow_rate != nullptr) { + sensor::Sensor* tvbh = m_data_requests.get_sensor("tvbh"); + sensor::Sensor* tv = m_data_requests.get_sensor("tv"); + sensor::Sensor* tr = m_data_requests.get_sensor("tr"); + + float value = 0; + if (mode_of_operating->state == "Warmwasserbereitung" && tv != nullptr && tr != nullptr) { + value = (tv->state - tr->state) * (4.19 * flow_rate->state) / 3600.0f; + } else if ((mode_of_operating->state == "Heizen" || mode_of_operating->state == "Kühlen") && tvbh != nullptr && tr != nullptr) { + value = (tvbh->state - tr->state) * (4.19 * flow_rate->state) / 3600.0f; + } + thermal_power->publish_state(value); + } + } +} + +///////////////// Texts ///////////////// +void DaikinRotexCanComponent::custom_request(std::string const& value) { + const uint32_t can_id = 0x680; + const bool use_extended_id = false; + + const TMessage buffer = Utils::str_to_bytes(value); + + if (!buffer.empty()) { + esphome::esp32_can::ESP32Can* pCanbus = m_data_requests.getCanbus(); + pCanbus->send_data(can_id, use_extended_id, { buffer.begin(), buffer.end() }); + + Utils::log("sendGet", "can_id<%s> data<%s>", + Utils::to_hex(can_id).c_str(), value.c_str(), Utils::to_hex(buffer).c_str()); + } +} + +///////////////// Selects ///////////////// +void DaikinRotexCanComponent::set_generic_select(std::string const& id, std::string const& state) { + for (auto& entity_conf : m_accessor.get_entities()) { + if (entity_conf.id == id && dynamic_cast(entity_conf.pEntity) != nullptr) { + auto it = entity_conf.map.findByValue(state); + if (it != entity_conf.map.end()) { + m_data_requests.sendSet(entity_conf.pEntity->get_name(), it->first); + } else { + ESP_LOGE(TAG, "set_generic_select(%s, %s) => Invalid value!", id.c_str(), state.c_str()); + } + break; + } + } +} + +///////////////// Numbers ///////////////// +void DaikinRotexCanComponent::set_generic_number(std::string const& id, float value) { + for (auto& entity_conf : m_accessor.get_entities()) { + if (entity_conf.id == id && dynamic_cast(entity_conf.pEntity) != nullptr) { + m_data_requests.sendSet(entity_conf.pEntity->get_name(), value); + break; + } + } +} + +///////////////// Buttons ///////////////// +void DaikinRotexCanComponent::dhw_run() { + TRequest const* pRequest = m_data_requests.get("target_hot_water_temperature"); + if (pRequest != nullptr) { + number::Number* pNumber = pRequest->get_number(); + if (pNumber != nullptr) { + const float temp = pNumber->state; + const std::string name = pRequest->getName(); + + m_data_requests.sendSet(name, 70); + + call_later([name, temp, this](){ + m_data_requests.sendSet(name, temp); + }, 10*1000); + } else { + ESP_LOGE("dhw_rum", "Request doesn't have a Number!"); + } + } else { + ESP_LOGE("dhw_rum", "Request couldn't be found!"); + } +} + +void DaikinRotexCanComponent::dump() { + const char* DUMP = "dump"; + + ESP_LOGI(DUMP, "------------------------------------------"); + ESP_LOGI(DUMP, "------------ DUMP %d Entities ------------", m_data_requests.size()); + ESP_LOGI(DUMP, "------------------------------------------"); + + for (auto index = 0; index < m_data_requests.size(); ++index) { + TRequest const* pRequest = m_data_requests.get(index); + if (pRequest != nullptr) { + EntityBase* pEntity = pRequest->get_entity(); + if (sensor::Sensor* pSensor = dynamic_cast(pEntity)) { + ESP_LOGI(DUMP, "%s: %f", pSensor->get_name().c_str(), pSensor->get_state()); + } else if (binary_sensor::BinarySensor* pBinarySensor = dynamic_cast(pEntity)) { + ESP_LOGI(DUMP, "%s: %d", pBinarySensor->get_name().c_str(), pBinarySensor->state); + } else if (number::Number* pNumber = dynamic_cast(pEntity)) { + ESP_LOGI(DUMP, "%s: %f", pNumber->get_name().c_str(), pNumber->state); + } else if (text_sensor::TextSensor* pTextSensor = dynamic_cast(pEntity)) { + ESP_LOGI(DUMP, "%s: %s", pTextSensor->get_name().c_str(), pTextSensor->get_state().c_str()); + } else if (select::Select* pSelect = dynamic_cast(pEntity)) { + ESP_LOGI(DUMP, "%s: %s", pSelect->get_name().c_str(), pSelect->state.c_str()); + } + } else { + ESP_LOGE(DUMP, "Entity with index<%d> not found!", index); + } + } + ESP_LOGI(DUMP, "------------------------------------------"); +} + +void DaikinRotexCanComponent::run_dhw_lambdas() { + if (m_accessor.getDaikinRotexCanComponent() != nullptr) { + if (!m_dhw_run_lambdas.empty()) { + auto& lambda = m_dhw_run_lambdas.front(); + lambda(); + m_dhw_run_lambdas.pop_front(); + } + } +} + +void DaikinRotexCanComponent::loop() { + m_data_requests.sendNextPendingGet(); + for (auto it = m_later_calls.begin(); it != m_later_calls.end(); ) { + if (millis() > it->second) { + it->first(); + it = m_later_calls.erase(it); + } else { + ++it; + } + } +} + +void DaikinRotexCanComponent::handle(uint32_t can_id, std::vector const& data) { + TMessage message; + std::copy_n(data.begin(), 7, message.begin()); + m_data_requests.handle(can_id, message); +} + +void DaikinRotexCanComponent::dump_config() { + ESP_LOGCONFIG(TAG, "DaikinRotexCanComponent"); +} + +void DaikinRotexCanComponent::throwPeriodicError(std::string const& message) { + call_later([message, this]() { + ESP_LOGE(TAG, message.c_str()); + throwPeriodicError(message); + }, 15 * 1000); +} + +} // namespace daikin_rotex_can +} // namespace esphome \ No newline at end of file diff --git a/components/daikin_rotex_can/daikin_rotex_can.h b/components/daikin_rotex_can/daikin_rotex_can.h new file mode 100644 index 0000000..16828e6 --- /dev/null +++ b/components/daikin_rotex_can/daikin_rotex_can.h @@ -0,0 +1,90 @@ +#pragma once + +#include "esphome/components/daikin_rotex_can/requests.h" +#include "esphome/components/daikin_rotex_can/Accessor.h" +#include "esphome/components/esp32_can/esp32_can.h" +#include "esphome/core/component.h" +#include + +namespace esphome { +namespace daikin_rotex_can { + +class DaikinRotexCanComponent: public Component { +public: + using TVoidFunc = std::function; + + DaikinRotexCanComponent(); + void setup() override; + void loop() override; + void dump_config() override; + + void set_canbus(esphome::esp32_can::ESP32Can* pCanbus); + void set_update_interval(uint16_t seconds) {} // dummy + + // Texts + void custom_request(std::string const& value); + + // Selects + void set_generic_select(std::string const& id, std::string const& state); + + // Numbers + void set_generic_number(std::string const& id, float value); + + // Buttons + void dhw_run(); + void dump(); + + Accessor& getAccessor() { return m_accessor; } + + void handle(uint32_t can_id, std::vector const& data); + + void run_dhw_lambdas(); + void call_later(TVoidFunc lambda, uint32_t timeout = 0u) { + const uint32_t timestamp = millis(); + m_later_calls.push_back({lambda, timestamp + timeout}); + } + + void update_thermal_power(); + +private: + + using TCanbusAutomation = esphome::Automation, uint32_t, bool>; + using TCanbusAction = esphome::Action, uint32_t, bool>; + + class MyAction : public TCanbusAction { + public: + MyAction(DaikinRotexCanComponent* pParent): m_pParent(pParent) {} + protected: + virtual void play(std::vector data, uint32_t can_id, bool remote_transmission_request) override { + m_pParent->handle(can_id, data); + } + private: + DaikinRotexCanComponent* m_pParent; + }; + + void updateState(std::string const& id); + + float getSensorState(std::string const& name); + void throwPeriodicError(std::string const& message); + + Accessor m_accessor; + TRequests m_data_requests; + std::shared_ptr m_canbus_trigger; + std::shared_ptr m_canbus_automation; + std::shared_ptr m_canbus_action; + std::list> m_later_calls; + std::list m_dhw_run_lambdas; +}; + +inline void DaikinRotexCanComponent::set_canbus(esphome::esp32_can::ESP32Can* pCanbus) { + m_data_requests.setCanbus(pCanbus); + + m_canbus_trigger = std::make_shared(pCanbus, 0, 0, false); // Listen to all can messages + m_canbus_automation = std::make_shared(m_canbus_trigger.get()); + m_canbus_action = std::make_shared(this); + m_canbus_automation->add_action(m_canbus_action.get()); + pCanbus->add_trigger(m_canbus_trigger.get()); +} + +} // namespace daikin_rotex_can +} // namespace esphome \ No newline at end of file diff --git a/components/daikin_rotex_can/numbers.cpp b/components/daikin_rotex_can/numbers.cpp new file mode 100644 index 0000000..7c694e0 --- /dev/null +++ b/components/daikin_rotex_can/numbers.cpp @@ -0,0 +1,12 @@ +#include "esphome/components/daikin_rotex_can/numbers.h" + +namespace esphome { +namespace daikin_rotex_can { + +void GenericNumber::control(float value) { + this->publish_state(value); + this->parent_->set_generic_number(m_id, value); +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/components/daikin_rotex_can/numbers.h b/components/daikin_rotex_can/numbers.h new file mode 100644 index 0000000..4b9825f --- /dev/null +++ b/components/daikin_rotex_can/numbers.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/components/daikin_rotex_can/daikin_rotex_can.h" + +namespace esphome { +namespace daikin_rotex_can { + +class GenericNumber : public number::Number, public Parented { +public: + GenericNumber() = default; + void set_id(std::string const& id) { m_id = id; } +protected: + void control(float value) override; +private: + std::string m_id; +}; + +} +} diff --git a/components/daikin_rotex_can/request.cpp b/components/daikin_rotex_can/request.cpp new file mode 100644 index 0000000..9e11676 --- /dev/null +++ b/components/daikin_rotex_can/request.cpp @@ -0,0 +1,110 @@ +#include "esphome/components/daikin_rotex_can/request.h" +#include "esphome/components/esp32_can/esp32_can.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace daikin_rotex_can { + +std::array TRequest::calculate_reponse(TMessage const& message) { + const uint16_t DC = 0xFFFF; + std::array response = {DC, DC, DC, DC, DC, DC, DC}; + if (message[2] == 0xFA) { // https://github.com/crycode-de/ioBroker.canbus/blob/master/well-known-messages/configs/rotex-hpsu.md + response[2] = message[2]; + response[3] = message[3]; + response[4] = message[4]; + } else { + response[2] = message[2]; + } + return response; +} + + +bool TRequest::isGetInProgress() const { + uint32_t mil = millis(); + return m_last_get_timestamp > m_last_handle_timestamp && ((mil - m_last_get_timestamp) < 3*1000); // Consider 3 sek => package is lost +} + +bool TRequest::isMatch(uint32_t can_id, TMessage const& responseData) const { + const uint16_t response_can_id = (m_command[0] & 0xF0) * 8 + (m_command[1] & 0x0F); + + //if (can_id == response_can_id()) + { + if ((responseData[0] & 0x0F) == 0x02) { // is a response + for (uint32_t index = 0; index < responseData.size(); ++index) { + if (m_expected_reponse[index] != DC && responseData[index] != m_expected_reponse[index]) { + return false; + } + } + return responseData.size() == 7; + } + } + return false; +} + +bool TRequest::handle(uint32_t can_id, TMessage const& responseData, uint32_t timestamp) { + if (isMatch(can_id, responseData)) { + const TVariant variant = m_lambda(responseData); + std::string value; + if (std::holds_alternative(variant)) { + value = std::to_string(std::get(variant)); + } else if (std::holds_alternative(variant)) { + value = std::to_string(std::get(variant)); + } else if (std::holds_alternative(variant)) { + value = std::to_string(std::get(variant)); + } else if (std::holds_alternative(variant)) { + value = std::get(variant); + } else if (std::holds_alternative(variant)) { + value = std::get(variant); + } else { + value = "Unsupported value type!"; + } + + Utils::log("handle ", "%s<%s> can_id<%s> data<%s>", + getName().c_str(), value.c_str(), Utils::to_hex(can_id).c_str(), Utils::to_hex(responseData).c_str()); + + m_last_handle_timestamp = timestamp; + return true; + } + return false; +} + +bool TRequest::sendGet(esphome::esp32_can::ESP32Can* pCanBus) { + if (pCanBus == nullptr) { + ESP_LOGE("sendGet", "pCanbus is null!"); + return false; + } + + const uint32_t can_id = 0x680; + const bool use_extended_id = false; + + pCanBus->send_data(can_id, use_extended_id, { m_command.begin(), m_command.end() }); + + Utils::log("sendGet", "%s can_id<%s> command<%s>", + getName().c_str(), Utils::to_hex(can_id).c_str(), Utils::to_hex(m_command).c_str()); + + m_last_get_timestamp = millis(); + return true; +} + +bool TRequest::sendSet(esphome::esp32_can::ESP32Can* pCanBus, float value) { + if (pCanBus == nullptr) { + ESP_LOGE("sendSet", "pCanbus is null!"); + return false; + } + + const uint32_t can_id = 0x680; + const bool use_extended_id = false; + + auto command = m_set_lambda(value); + + pCanBus->send_data(can_id, use_extended_id, { command.begin(), command.end() }); + Utils::log("sendSet", "name<%s> value<%f> can_id<%s> command<%s>", + getName().c_str(), value, Utils::to_hex(can_id).c_str(), Utils::to_hex(command).c_str()); + + sendGet(pCanBus); + + return true; +} + +} +} \ No newline at end of file diff --git a/components/daikin_rotex_can/request.h b/components/daikin_rotex_can/request.h new file mode 100644 index 0000000..767233f --- /dev/null +++ b/components/daikin_rotex_can/request.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/components/daikin_rotex_can/types.h" +#include "esphome/components/daikin_rotex_can/utils.h" +#include "esphome/components/esp32_can/esp32_can.h" +#include + +namespace esphome { +namespace daikin_rotex_can { + +class TRequest +{ + static const uint16_t DC = 0xFFFF; // Don't care + +public: + using TVariant = std::variant; + using TGetLambda = std::function; + using TSetLambda = std::function; +public: + TRequest( + std::string const& id, + TMessage const& command, + EntityBase* entity, + TGetLambda lambda, + TSetLambda setLambda, + uint16_t update_interval) + : m_id(id) + , m_command(command) + , m_expected_reponse(TRequest::calculate_reponse(command)) + , m_entity(entity) + , m_lambda(lambda) + , m_set_lambda(setLambda) + , m_last_handle_timestamp(0u) + , m_last_get_timestamp(0u) + , m_update_interval(update_interval) + { + } + + std::string const& get_id() const { return m_id; } + + std::string getName() const { + return m_entity != nullptr ? m_entity->get_name().str() : ""; + } + + EntityBase* get_entity() const { + return m_entity; + } + + sensor::Sensor* get_sensor() const { + return dynamic_cast(m_entity); + } + + number::Number* get_number() const { + return dynamic_cast(m_entity); + } + + bool isGetSupported() const { + return m_entity != nullptr; + } + + uint32_t getLastUpdate() const { + return m_last_handle_timestamp; + } + + bool isMatch(uint32_t can_id, TMessage const& responseData) const; + bool handle(uint32_t can_id, TMessage const& responseData, uint32_t timestamp); + + bool sendGet(esphome::esp32_can::ESP32Can* pCanBus); + bool sendSet(esphome::esp32_can::ESP32Can* pCanBus, float value); + + bool isGetNeeded() const; + + bool isGetInProgress() const; + uint16_t get_update_interval() const { return m_update_interval; } + + static std::array calculate_reponse(TMessage const& message); + + std::string string() { + return Utils::format( + "TRequest", + getName().c_str(), + Utils::to_hex(m_command).c_str() + ); + } + +private: + std::string m_id; + TMessage m_command; + std::array m_expected_reponse; + EntityBase* m_entity; + TGetLambda m_lambda; + TSetLambda m_set_lambda; + uint32_t m_last_handle_timestamp; + uint32_t m_last_get_timestamp; + uint16_t m_update_interval; +}; + +inline bool TRequest::isGetNeeded() const { + const uint32_t update_interval = get_update_interval() * 1000; + return getLastUpdate() == 0 || (millis() > (getLastUpdate() + update_interval)); +} + +} +} + diff --git a/components/daikin_rotex_can/requests.cpp b/components/daikin_rotex_can/requests.cpp new file mode 100644 index 0000000..2a653d4 --- /dev/null +++ b/components/daikin_rotex_can/requests.cpp @@ -0,0 +1,99 @@ +#include "esphome/components/daikin_rotex_can/requests.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace daikin_rotex_can { + +TRequests::TRequests() +: m_requests() +, m_pCanbus(nullptr) +{ +} + +void TRequests::add(esphome::daikin_rotex_can::TRequest const& request) { + m_requests.push_back(request); +} + +void TRequests::removeInvalidRequests() { + m_requests.erase( + std::remove_if( + m_requests.begin(), + m_requests.end(), + [](TRequest const& request) { return !request.isGetSupported(); } + ), + m_requests.end() + ); +} + +EntityBase* TRequests::get_entity(std::string const& id) { + TRequest const* pRequest = get(id); + if (pRequest != nullptr) { + return pRequest->get_entity(); + } else { + ESP_LOGE("get_entity", "Entity not found: %s", id.c_str()); + } + return nullptr; +} + +bool TRequests::sendNextPendingGet() { + TRequest* pRequest = getNextRequestToSend(); + if (pRequest != nullptr) { + return pRequest->sendGet(m_pCanbus); + } + return false; +} + +void TRequests::sendSet(std::string const& request_name, float value) { + const auto it = std::find_if(m_requests.begin(), m_requests.end(), + [&request_name](auto& request) { return request.getName() == request_name; } + ); + if (it != m_requests.end()) { + it->sendSet(m_pCanbus, value); + } else { + ESP_LOGE("sendSet", "Unknown request: %s", request_name.c_str()); + } +} + +void TRequests::handle(uint32_t can_id, TMessage const& responseData) { + bool bHandled = false; + const uint32_t timestamp = millis(); + for (auto& request : m_requests) { + if (request.handle(can_id, responseData, timestamp)) { + bHandled = true; + break; + } + } + if (!bHandled) { + Utils::log("unhandled", "can_id<%s> data<%s>", Utils::to_hex(can_id).c_str(), Utils::to_hex(responseData).c_str()); + } +} + +TRequest const* TRequests::get(std::string const& id) const { + for (auto& request: m_requests) { + if (request.get_id() == id) { + return &request; + } + } + return nullptr; +} + +TRequest* TRequests::getNextRequestToSend() { + const uint32_t timestamp = millis(); + + for (auto& request : m_requests) { + if (request.isGetInProgress()) { + return nullptr; + } + } + + for (auto& request : m_requests) { + if (request.isGetNeeded()) { + return &request; + } + } + return nullptr; +} + + +} +} \ No newline at end of file diff --git a/components/daikin_rotex_can/requests.h b/components/daikin_rotex_can/requests.h new file mode 100644 index 0000000..7c64f2e --- /dev/null +++ b/components/daikin_rotex_can/requests.h @@ -0,0 +1,65 @@ +#pragma once + +#include "esphome/components/daikin_rotex_can/request.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include + +namespace esphome { +namespace daikin_rotex_can { + +class TRequests { +public: + TRequests(); + void add(esphome::daikin_rotex_can::TRequest const& request); + + void removeInvalidRequests(); + + void setCanbus(esphome::esp32_can::ESP32Can* pCanbus); + esphome::esp32_can::ESP32Can* getCanbus() const; + + uint32_t size() const; + TRequest const* get(uint32_t index) const; + TRequest const* get(std::string const& id) const; + + EntityBase* get_entity(std::string const& id); + sensor::Sensor* get_sensor(std::string const& id); + text_sensor::TextSensor* get_text_sensor(std::string const& id); + + bool sendNextPendingGet(); + void sendSet(std::string const& request_name, float value); + void handle(uint32_t can_id, TMessage const& responseData); + +private: + TRequest* getNextRequestToSend(); + + std::vector m_requests; + esphome::esp32_can::ESP32Can* m_pCanbus; +}; + +inline void TRequests::setCanbus(esphome::esp32_can::ESP32Can* pCanbus) { + m_pCanbus = pCanbus; +} + +inline esphome::esp32_can::ESP32Can* TRequests::getCanbus() const { + return m_pCanbus; +} + +inline uint32_t TRequests::size() const { + return m_requests.size(); +} + +inline TRequest const* TRequests::get(uint32_t index) const { + return (index >= 0 && index < m_requests.size()) ? &m_requests[index] : nullptr; +} + +inline sensor::Sensor* TRequests::get_sensor(std::string const& id) { + return Utils::toSensor(get_entity(id)); +} + +inline text_sensor::TextSensor* TRequests::get_text_sensor(std::string const& id) { + return Utils::toTextSensor(get_entity(id)); +} + + +} +} \ No newline at end of file diff --git a/components/daikin_rotex_can/selects.cpp b/components/daikin_rotex_can/selects.cpp new file mode 100644 index 0000000..cf00879 --- /dev/null +++ b/components/daikin_rotex_can/selects.cpp @@ -0,0 +1,12 @@ +#include "esphome/components/daikin_rotex_can/selects.h" + +namespace esphome { +namespace daikin_rotex_can { + +void GenericSelect::control(const std::string &value) { + this->publish_state(value); + this->parent_->set_generic_select(m_id, state); +} + +} // namespace daikin_rotex_can +} // namespace esphome diff --git a/components/daikin_rotex_can/selects.h b/components/daikin_rotex_can/selects.h new file mode 100644 index 0000000..ba4f6f2 --- /dev/null +++ b/components/daikin_rotex_can/selects.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/components/daikin_rotex_can/daikin_rotex_can.h" + +namespace esphome { +namespace daikin_rotex_can { + +class GenericSelect : public select::Select, public Parented { +public: + GenericSelect() = default; + void set_id(std::string const& id) { m_id = id; } +protected: + void control(const std::string &value) override; +private: + std::string m_id; +}; + +} // namespace ld2410 +} // namespace esphome diff --git a/components/daikin_rotex_can/texts.cpp b/components/daikin_rotex_can/texts.cpp new file mode 100644 index 0000000..3efaaa3 --- /dev/null +++ b/components/daikin_rotex_can/texts.cpp @@ -0,0 +1,25 @@ +#include "esphome/components/daikin_rotex_can/texts.h" + +namespace esphome { +namespace daikin_rotex_can { + +LogFilterText::LogFilterText() { + this->publish_state(""); +} + +void LogFilterText::control(const std::string &value) { + this->publish_state(value); + Utils::g_log_filter = value; +} + +CustomRequestText::CustomRequestText() { + this->publish_state(""); +} + +void CustomRequestText::control(const std::string &value) { + this->publish_state(value); + this->parent_->custom_request(value); +} + +} // namespace seeed_mr24hpc1 +} // namespace esphome diff --git a/components/daikin_rotex_can/texts.h b/components/daikin_rotex_can/texts.h new file mode 100644 index 0000000..83499dc --- /dev/null +++ b/components/daikin_rotex_can/texts.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/text/text.h" +#include "esphome/components/daikin_rotex_can/daikin_rotex_can.h" + +namespace esphome { +namespace daikin_rotex_can { + +class LogFilterText : public text::Text, public Parented { +public: + LogFilterText(); +protected: + void control(const std::string &value) override; +}; +class CustomRequestText : public text::Text, public Parented { +public: + CustomRequestText(); +protected: + void control(const std::string &value) override; +}; + +} +} diff --git a/components/daikin_rotex_can/types.h b/components/daikin_rotex_can/types.h new file mode 100644 index 0000000..3120255 --- /dev/null +++ b/components/daikin_rotex_can/types.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +namespace esphome { +namespace daikin_rotex_can { + +using TMessage = std::array; + +} +} \ No newline at end of file diff --git a/components/daikin_rotex_can/utils.cpp b/components/daikin_rotex_can/utils.cpp new file mode 100644 index 0000000..dc8b113 --- /dev/null +++ b/components/daikin_rotex_can/utils.cpp @@ -0,0 +1,167 @@ +#include "esphome/components/daikin_rotex_can/utils.h" +#include + +namespace esphome { +namespace daikin_rotex_can { + +std::string Utils::g_log_filter = ""; +static const char* TAG = "Utils"; + +bool Utils::find(std::string const& haystack, std::string const& needle) { + auto it = std::search( + haystack.begin(), haystack.end(), + needle.begin(), needle.end(), + [](unsigned char ch1, unsigned char ch2) { return std::toupper(ch1) == std::toupper(ch2); } + ); + return (it != haystack.end()); +} + +std::vector Utils::split(std::string const& str) { + std::string segment; + std::istringstream iss(str); + std::vector result; + + while (std::getline(iss, segment, '|')) { + if (!segment.empty()) { + result.push_back(segment); + } + } + + return result; +} + +std::string Utils::to_hex(uint32_t value) { + char hex_string[20]; + sprintf(hex_string, "0x%02X", value); + return std::string(hex_string); +} + +std::string Utils::to_hex(TMessage const& data) { + std::stringstream str; + str.setf(std::ios_base::hex, std::ios::basefield); + str.setf(std::ios_base::uppercase); + str.fill('0'); + + bool first = true; + for (uint8_t chr : data) + { + if (first) { + first = false; + } else { + str << " "; + } + str << std::setw(2) << (unsigned short)(std::byte)chr; + } + return str.str(); +} + +TMessage Utils::str_to_bytes(const std::string& str) { + TMessage bytes; + std::stringstream ss(str); + std::string byteStr; + + uint8_t index = 0; + while (ss >> byteStr) { + const uint8_t byte = static_cast(std::stoi(byteStr, nullptr, 16)); + bytes[index++] = byte; + } + + return bytes; +} + +TMessage Utils::str_to_bytes_array8(const std::string& str) { + TMessage byte_array{}; + + std::string cleaned_str = std::regex_replace(str, std::regex("[^0-9A-Fa-f\\s]+"), ""); + cleaned_str = std::regex_replace(cleaned_str, std::regex("\\s+"), " "); + + std::stringstream ss(cleaned_str); + std::string byte_str; + size_t index = 0; + + while (ss >> byte_str && index < byte_array.size()) { + byte_array[index++] = static_cast(std::stoul(byte_str, nullptr, 16)); + } + + return byte_array; +} + +std::map Utils::str_to_map(const std::string& input) { + std::map result; + std::stringstream ss(input); + std::string pair; + + while (std::getline(ss, pair, '|')) { + size_t pos = pair.find(':'); + if (pos != std::string::npos) { + std::string keyStr = pair.substr(0, pos); + std::string value = pair.substr(pos + 1); + + uint8_t key = static_cast(std::strtoul(keyStr.c_str(), nullptr, 16)); + + result[key] = value; + } + } + + return result; +} + +void Utils::setBytes(TMessage& data, uint16_t value, uint8_t offset, uint8_t len) { + if (len == 1) { + data[offset] = value & 0xFF; + } else if (len == 2) { + data[offset] = (value >> 8) & 0xFF; + data[offset + 1] = value & 0xFF; + } else { + ESP_LOGE("write", "Invalid len: %d", len); + } +} + +sensor::Sensor* Utils::toSensor(EntityBase* pEntity) { + if (sensor::Sensor* pSensor = dynamic_cast(pEntity)) { + return pSensor; + } else { + ESP_LOGE(TAG, "Entity is not a sensor: %s", pEntity->get_name().c_str()); + return nullptr; + } +} + +text_sensor::TextSensor* Utils::toTextSensor(EntityBase* pEntity) { + if (text_sensor::TextSensor* pTextSensor = dynamic_cast(pEntity)) { + return pTextSensor; + } else { + ESP_LOGE(TAG, "Entity is not a text sensor: %s", pEntity->get_name().c_str()); + return nullptr; + } +} + +binary_sensor::BinarySensor* Utils::toBinarySensor(EntityBase* pEntity) { + if (binary_sensor::BinarySensor* pBinarySensor = dynamic_cast(pEntity)) { + return pBinarySensor; + } else { + ESP_LOGE(TAG, "Entity is not a binary sensor: %s", pEntity->get_name().c_str()); + return nullptr; + } +} + +select::Select* Utils::toSelect(EntityBase* pEntity) { + if (select::Select* pSelect = dynamic_cast(pEntity)) { + return pSelect; + } else { + ESP_LOGE(TAG, "Entity is not a select: %s", pEntity->get_name().c_str()); + return nullptr; + } +} + +number::Number* Utils::toNumber(EntityBase* pEntity) { + if (number::Number* pNumber = dynamic_cast(pEntity)) { + return pNumber; + } else { + ESP_LOGE(TAG, "Entity is not a number: %s", pEntity->get_name().c_str()); + return nullptr; + } +} + + +} +} \ No newline at end of file diff --git a/components/daikin_rotex_can/utils.h b/components/daikin_rotex_can/utils.h new file mode 100644 index 0000000..9759991 --- /dev/null +++ b/components/daikin_rotex_can/utils.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/components/daikin_rotex_can/types.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/select/select.h" +#include "esphome/components/number/number.h" +#include "esphome/core/log.h" +#include +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace daikin_rotex_can { + +class Utils { +public: + template + static std::string format(const std::string& format, Args... args) { + const auto size = std::snprintf(nullptr, 0, format.c_str(), args...) + 1; + const auto buffer = std::make_unique(size); + + std::snprintf(buffer.get(), size, format.c_str(), args...); + + return std::string(buffer.get(), buffer.get() + size - 1); + } + + static std::string to_hex(TMessage const& data); + static bool find(std::string const& haystack, std::string const& needle); + static std::vector split(std::string const& str); + static std::string to_hex(uint32_t value); + static TMessage str_to_bytes(const std::string& str); + static TMessage str_to_bytes_array8(const std::string& str); + static std::map str_to_map(const std::string& input); + static void setBytes(TMessage& data, uint16_t value, uint8_t offset, uint8_t len); + + template + static void log(std::string const& tag, std::string const& str_format, Args... args) { + const std::string formated = Utils::format(str_format, args...); + const std::string log_filter = g_log_filter; + bool found = log_filter.empty(); + if (!found) { + for (auto segment : Utils::split(log_filter)) { + if (Utils::find(tag, segment) || Utils::find(formated, segment)) { + found = true; + break; + } + } + } + if (found) { + ESP_LOGI(tag.c_str(), formated.c_str(), ""); + } + } + + static sensor::Sensor* toSensor(EntityBase*); + static text_sensor::TextSensor* toTextSensor(EntityBase*); + static binary_sensor::BinarySensor* toBinarySensor(EntityBase*); + static select::Select* toSelect(EntityBase*); + static number::Number* toNumber(EntityBase*); + + static std::string g_log_filter; +}; + +} +} \ No newline at end of file diff --git a/examples/full.yaml b/examples/full.yaml new file mode 100644 index 0000000..6b2f061 --- /dev/null +++ b/examples/full.yaml @@ -0,0 +1,265 @@ +esphome: + name: rotex + friendly_name: Rotex + platformio_options: + build_unflags: + - "-std=gnu++11" + - "-fno-rtti" + build_flags: + - "-std=gnu++17" + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: arduino + +external_components: + - source: github://wrfz/esphome-daikin-rotex-can + +logger: + level: INFO + +api: + encryption: + key: !secret api_encryption_key + +ota: + platform: esphome + password: !secret ota_password + +web_server: + version: 3 + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + ap: + ssid: "DaikinRotex Fallback Hotspot" + password: !secret fallback_hotspot_password + +canbus: + - platform: esp32_can + id: can_bus + can_id: 0x680 + rx_pin: GPIO2 + tx_pin: GPIO3 + bit_rate: 20kbps + +daikin_rotex_can: + id: rotext_hpsu + canbus_id: can_bus + update_interval: 30 + log_filter: + name: "Log Filter" + custom_request: + name: "Custom Request" + entities: + +# Info + + ext: + name: Ext + +#Übersicht + water_pressure: + name: "Wasserdruck" + t_hs: + name: "T-WE" +#T-WE Soll + t_ext: # Oder gehört hier temperature_outside rein? + name: "T-Aussen" +#T-WW +#T-WW Soll +#T-Rücklauf + flow_rate: + name: "Durchfluss" + update_interval: 5 +#T-HK +#T-HK Soll + status_kesselpumpe: + name: "Status Kesselpumpe" + runtime_compressor: + name: "Laufzeit Compressor" + runtime_pump: + name: "Laufzeit Pump" + dhw_mixer_position: + name: "DHW Mischer Position" + qboh: + name: "EHS für DHW" + ehs_for_ch: + name: "EHS fuer CH" +#Energie Kühlung + qch: + name: "Energie Heizung" + total_energy_produced: + name: "Erzeugte Energie Gesamt" + qdhw: + name: "Energie für WW" +#WE Typ +#SwNr B1/U1 +#SwNr Regler +#SwNr RTXRT + +#Konfiguration + +#Konfiguration -> Installation +#... + outdoor_unit: + name: "Aussengerät" + indoor_unit: + name: "Innengerät" + function_ehs: + name: "Funktion EHS" + ch_support: + name: "HZ Unterstützung" +#Bivalenzfunktion + smart_grid: + name: "Smart Grid" + sg_mode: + name: "SG Modus" +#HT/NT Funktion +#HT/NT Anschluss +#Raumthermostat +#Interlinkfunktion +#Konfig MFR 1 +#Entlüftungsfunktion +#PWM Konfig +#... +#Sensor Konfig +#AF Anspassung +#Terminal Adresse +#Konfig System + +#Konfiguration -> Anlagenkonfiguration + + power_dhw: + name: "Leistung WW" + power_ehs_1: + name: "Leistung EHS Stufe 1" + power_ehs_2: + name: "Leistung EHS Stufe 2" + power_biv: + name: "Leistung BIV" + tdiff_dhw_ch: + name: "TDiff-WW HZU" +#Max Temp Heizung +#Bivalenztemperatur + quiet: + name: "Flüsterbetrieb" +#Sonderfunkt Schaltk +#Wartezeit Sonderfunkt + t_dhw_1_min: + name: "Schaltschwelle TDHW" + delta_temp_ch: + name: "Spreizung MOD HZ" +#Durchfluss Ber +#Anpassung T-VL Heizen +#Anpassung T-VL Kühlen +#Min Druck +#Max Druck +#Soll Druck +#Max Druckverlust +#Relaistest + +# Konfiguration -> HZK Konfig + hk_function: + name: "HK Funktion" +#T-Frostschutz +#Gebäudedämumng +#Estrich +#Estrichprogramm + +# Konfiguration -> HZK Konfig -> Heizen + +#Heizgrenze Tag +#Heizgrenze Nacht + heating_curve: + name: "Heizkurve" +#Raumeinfluss +#RF Anpassung + flow_temperature_day: + name: "T Vorlauf Tag" +#T-Vorlauf Nacht +#Max T-Vorlauf +#Min T-Vorlauf +#Heizk Adaption + +# Konfiguration -> HZK Konfig -> Kühlen + +#StartKühlen A-Temp +#MaxKühlen A-Temp +#VL-SollStartKühlen +#VL-SollMaxKühlen +#MinVL-SollKühlen +#T-VLKühlen +#T_H/K Umschaltung +#KühlsollwertKorr + +#Konfiguration -> WW Konfig + + circulation_with_dhw_program: + name: "Zirk mit WW-Prog" + circulation_interval_on: + name: "ZirkInterval An" + circulation_interval_off: + name: "ZirkInterval Aus" + antileg_day: + name: "AntilegTag" +#AntilegZeit + antileg_temp: + name: "AntilegTemp" + max_dhw_loading: + name: "Max WW Ladezeit" + dhw_off_time: + name: "WW Sperrzeit" + +# Others + + electric_heater: + name: "Heizstäbe" + thermal_power: + name: "Thermische Leistung" + +# To be categorized + + bypass_valve: + name: "BPV" + circulation_pump: + name: "Umwaelzpumpe" + circulation_pump_min: + name: "Umwälzpumpe Min" + circulation_pump_max: + name: "Umwälzpumpe Max" + delta_temp_dhw: + name: "Spreizung MOD WW" + dhw_run: + name: "Warmwasser bereiten" + error_code: + name: "Fehlercode" + max_target_flow_temp: + name: "Max VL Soll" + min_target_flow_temp: + name: "Min VL Soll" + mode_of_operating: + name: "Betriebsart" + operating_mode: + name: "Betriebsmodus" + status_kompressor: + name: "Status Kompressor" + target_room1_temperature: + name: "Raumsoll 1" + tdhw1: + name: "Warmwassertemperatur" + temperature_outside: + name: "Aussentemperatur" + tr: + name: "Ruecklauftemperatur Heizung" + tv: + name: "Heizkreis Vorlauf (TV)" + tvbh: + name: "Vorlauftemperatur Heizung (TVBH)" + target_hot_water_temperature: + name: "T-WW-Soll1" + target_supply_temperature: + name: "Vorlauf Soll" diff --git a/examples/minimal.yaml b/examples/minimal.yaml new file mode 100644 index 0000000..5f7a231 --- /dev/null +++ b/examples/minimal.yaml @@ -0,0 +1,32 @@ +esphome: + name: rotex + friendly_name: Rotex + platformio_options: + build_unflags: + - "-std=gnu++11" + - "-fno-rtti" + build_flags: + - "-std=gnu++17" + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: arduino + +external_components: # Automatically loads the *esphome-daikin-rotex-can* component from github during compilation + - source: github://wrfz/esphome-daikin-rotex-can + +canbus: # Required for communication with the Daikin or Rotex heat pump + - platform: esp32_can + id: can_bus + can_id: 0x680 + rx_pin: GPIO2 + tx_pin: GPIO3 + bit_rate: 20kbps # HPSU heat pumps require a mandatory baud rate of 20kbs! + +daikin_rotex_can: + id: rotext_hpsu + canbus_id: can_bus + entities: + tdhw1: + name: "Warmwassertemperatur" diff --git a/examples/options.yaml b/examples/options.yaml new file mode 100644 index 0000000..25b5f87 --- /dev/null +++ b/examples/options.yaml @@ -0,0 +1,13 @@ +daikin_rotex_can: + id: rotext_hpsu + canbus_id: can_bus + update_interval: 60 # Set the global update interval to 60 seconds. Default is 30 + entities: + tdhw1: + name: "Warmwassertemperatur" + water_flow: + name: "Durchfluss" + update_interval: 10 # Overwrites the global update interval with 10 seconds. The global interval applies if not set here + circulation_pump_max: + name: "Umwälzpumpe Max" + mode: "SLIDER" # Displays the circulation_pump_max number control as a slider