31 Commits

Author SHA1 Message Date
Proddy
34a2b20be8 Merge pull request #2978 from MichaelDvP/dev
add basflowtemp #2969, add pumpkick #2965, add reset HP #2933, fix custom brand
2026-03-13 19:58:03 +01:00
MichaelDvP
f1fc8d9aae update testdata 2026-03-13 16:52:48 +01:00
MichaelDvP
b04355e3e1 update asyncwebserver 2026-03-13 10:32:33 +01:00
MichaelDvP
cd3ae5cdf2 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-03-13 10:22:24 +01:00
MichaelDvP
a261ca23af add baseflowtemp #2969 2026-03-13 10:15:51 +01:00
MichaelDvP
cb96904a5c fix custom brand use after free of c_str() in json. 2026-03-13 10:15:00 +01:00
MichaelDvP
4a2d78f8e1 minflowtemp taken from offset 13 or 8 2026-03-11 18:43:25 +01:00
Proddy
f5af4fb52f Merge pull request #2975 from mrkev-gh/esp32s3-no-psram
fix allowed pins for S32S3 without PSRAM
2026-03-11 17:36:38 +01:00
MichaelDvP
2037bc3a10 add reset of HP errors #2933, dev9 2026-03-07 12:02:09 +01:00
MichaelDvP
64d17d7c65 Test for minflowtemp 2026-03-07 11:43:12 +01:00
MichaelDvP
92e2633342 typo 2026-03-07 11:42:27 +01:00
mrkev-gh
96a7ea8a02 fix allowed pins for S32S3 without PSRAM
Some S32S3 do not have PSRAM (e.g. ESP32-S3FN8) and use those GPIO pins
2026-02-28 11:44:53 +01:00
MichaelDvP
5c4aaa4510 add pumpkick #2965, dev.8 2026-02-20 09:56:08 +01:00
Proddy
c05e1cb77b Merge pull request #2966 from MichaelDvP/dev
fixes for #2960 and #2962
2026-02-19 21:58:20 +01:00
MichaelDvP
5879ce4090 fix SRC mode setting from HA #2960 2026-02-18 08:14:47 +01:00
MichaelDvP
ac3e5c793c fix typo for SRC ha-climate creation 2026-02-17 10:09:22 +01:00
MichaelDvP
4326fb931b add prometheus metrics for analog/scheduler/custom #2962 2026-02-16 15:56:23 +01:00
MichaelDvP
ced7051ce7 add prometheus metrics for temperaturesensors 2026-02-16 12:05:45 +01:00
MichaelDvP
421da246ed fix SRC seltemp offset for auto mode #2960 2026-02-16 07:51:10 +01:00
MichaelDvP
148a721e17 read connect seltemp after mode/icon to create HA-climate 2026-02-15 16:49:21 +01:00
MichaelDvP
a811670c5a 3.8.2-dev.6, changelog 2026-02-15 12:03:33 +01:00
MichaelDvP
72f08a86cf fix SRC climate, #2960 2026-02-15 12:03:07 +01:00
MichaelDvP
27c471f45f set model for ems-esp devices, #2958 2026-02-15 12:02:36 +01:00
MichaelDvP
e303972d26 update AsyncWebserver and pkg 2026-02-15 12:01:50 +01:00
MichaelDvP
97bb03d703 add missing check for number mode change 2026-02-15 12:01:10 +01:00
Proddy
e9f77c1bde Merge pull request #2954 from MichaelDvP/dev
fix brand in HA
2026-02-12 17:54:41 +01:00
MichaelDvP
81cba6c0a8 fix brand in HA 2026-02-12 17:41:10 +01:00
Proddy
89029df25e Merge pull request #2953 from MichaelDvP/dev
customize device brand #2784
2026-02-12 13:19:46 +01:00
MichaelDvP
3463b6818d update testdata 2026-02-12 12:14:13 +01:00
MichaelDvP
349843e666 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-02-12 12:02:45 +01:00
MichaelDvP
96ae3bbbba customze device brand #2784 2026-02-12 12:01:39 +01:00
31 changed files with 1606 additions and 770 deletions

View File

@@ -7,10 +7,15 @@ For more details go to [emsesp.org](https://emsesp.org/).
## Added ## Added
- comfortpoint for BC400 [#2935](https://github.com/emsesp/EMS-ESP32/issues/2935) - comfortpoint for BC400 [#2935](https://github.com/emsesp/EMS-ESP32/issues/2935)
- customize device brand [#2784](https://github.com/emsesp/EMS-ESP32/issues/2784)
- set model for ems-esp devices temperature, analog, etc. [#2958](https://github.com/emsesp/EMS-ESP32/discussions/2958)
- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962)
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
## Fixed ## Fixed
- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) - SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960)
## Changed ## Changed
@@ -19,3 +24,4 @@ For more details go to [emsesp.org](https://emsesp.org/).
- remove wrong burnMinPower [#2918](https://github.com/emsesp/EMS-ESP32/issues/2918) - remove wrong burnMinPower [#2918](https://github.com/emsesp/EMS-ESP32/issues/2918)
- store scheduler active state to nvs [#2946](https://github.com/emsesp/EMS-ESP32/discussions/2946) - store scheduler active state to nvs [#2946](https://github.com/emsesp/EMS-ESP32/discussions/2946)
- translated modes `heat` and `eco` for HA-climate mode-str-tpl - translated modes `heat` and `eco` for HA-climate mode-str-tpl
- support `minflowtemp` and `baseflowtemp` [#2969](https://github.com/emsesp/EMS-ESP32/discussions/2969)

View File

@@ -1,5 +1,5 @@
{ {
"adapter": "react", "adapter": "react",
"baseLocale": "pl", "baseLocale": "pl",
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json" "$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json"
} }

View File

@@ -26,46 +26,46 @@
"@alova/adapter-xhr": "2.3.1", "@alova/adapter-xhr": "2.3.1",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.7", "@mui/icons-material": "^7.3.9",
"@mui/material": "^7.3.7", "@mui/material": "^7.3.9",
"@preact/compat": "^18.3.1", "@preact/compat": "^18.3.2",
"@table-library/react-table-library": "4.1.15", "@table-library/react-table-library": "4.1.15",
"alova": "3.5.0", "alova": "3.5.1",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"etag": "^1.8.1", "etag": "^1.8.1",
"formidable": "^3.5.4", "formidable": "^3.5.4",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"mime-types": "^3.0.2", "mime-types": "^3.0.2",
"preact": "^10.28.3", "preact": "^10.29.0",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
"react-icons": "^5.5.0", "react-icons": "^5.6.0",
"react-router": "^7.13.0", "react-router": "^7.13.1",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"typesafe-i18n": "^5.26.2", "typesafe-i18n": "^5.27.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.29.0", "@babel/core": "^7.29.0",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@preact/compat": "^18.3.1", "@preact/compat": "^18.3.2",
"@preact/preset-vite": "^2.10.3", "@preact/preset-vite": "^2.10.3",
"@trivago/prettier-plugin-sort-imports": "^6.0.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.2.2", "@types/node": "^25.5.0",
"@types/react": "^19.2.13", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"axe-core": "^4.11.1", "axe-core": "^4.11.1",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^10.0.0", "eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^7.0.1",
"terser": "^5.46.0", "terser": "^5.46.0",
"typescript-eslint": "^8.54.0", "typescript-eslint": "^8.57.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^6.1.0" "vite-tsconfig-paths": "^6.1.1"
}, },
"packageManager": "pnpm@10.29.2" "packageManager": "pnpm@10.32.1"
} }

1730
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -111,13 +111,14 @@ const Customizations = () => {
const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] = const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] =
useState<string>(''); // needed for API URL useState<string>(''); // needed for API URL
const [selectedDeviceName, setSelectedDeviceName] = useState<string>(''); const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
const [selectedDeviceBrand, setSelectedDeviceBrand] = useState<string>('');
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), { const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
immediate: false immediate: false
}); });
const { send: sendDeviceName } = useRequest( const { send: sendDeviceName } = useRequest(
(data: { id: number; name: string }) => writeDeviceName(data), (data: { id: number; name: string; brand: string }) => writeDeviceName(data),
{ {
immediate: false immediate: false
} }
@@ -267,6 +268,7 @@ const Customizations = () => {
if (device) { if (device) {
setSelectedDeviceTypeNameURL(device.url || ''); setSelectedDeviceTypeNameURL(device.url || '');
setSelectedDeviceName(device.n); setSelectedDeviceName(device.n);
setSelectedDeviceBrand(device.b);
} }
setNumChanges(0); setNumChanges(0);
setRestartNeeded(false); setRestartNeeded(false);
@@ -442,7 +444,11 @@ const Customizations = () => {
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]); }, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]);
const renameDevice = useCallback(async () => { const renameDevice = useCallback(async () => {
await sendDeviceName({ id: selectedDevice, name: selectedDeviceName }) await sendDeviceName({
id: selectedDevice,
name: selectedDeviceName,
brand: selectedDeviceBrand
})
.then(() => { .then(() => {
toast.success(LL.UPDATED_OF(LL.NAME(1))); toast.success(LL.UPDATED_OF(LL.NAME(1)));
}) })
@@ -453,7 +459,14 @@ const Customizations = () => {
setRename(false); setRename(false);
await fetchCoreData(); await fetchCoreData();
}); });
}, [selectedDevice, selectedDeviceName, sendDeviceName, LL, fetchCoreData]); }, [
selectedDevice,
selectedDeviceName,
selectedDeviceBrand,
sendDeviceName,
LL,
fetchCoreData
]);
const renderDeviceList = () => ( const renderDeviceList = () => (
<> <>
@@ -462,15 +475,26 @@ const Customizations = () => {
</Box> </Box>
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}> <Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
{rename ? ( {rename ? (
<TextField <>
name="device" <TextField
label={LL.EMS_DEVICE()} name="device"
fullWidth label={LL.EMS_DEVICE()}
variant="outlined" style={{ minWidth: '48%' }}
value={selectedDeviceName} variant="outlined"
onChange={(e) => setSelectedDeviceName(e.target.value)} value={selectedDeviceName}
margin="normal" onChange={(e) => setSelectedDeviceName(e.target.value)}
/> margin="normal"
/>
<TextField
name="brand"
label={LL.BRAND()}
style={{ minWidth: '48%' }}
variant="outlined"
value={selectedDeviceBrand}
onChange={(e) => setSelectedDeviceBrand(e.target.value)}
margin="normal"
/>
</>
) : ( ) : (
<TextField <TextField
name="device" name="device"

View File

@@ -106,7 +106,7 @@ board_build.filesystem = littlefs
lib_deps = lib_deps =
bblanchon/ArduinoJson @ 7.4.2 bblanchon/ArduinoJson @ 7.4.2
ESP32Async/AsyncTCP @ 3.4.10 ESP32Async/AsyncTCP @ 3.4.10
ESP32Async/ESPAsyncWebServer @ 3.9.6 ESP32Async/ESPAsyncWebServer @ 3.10.1
https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8 https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8

View File

@@ -316,6 +316,10 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
changed = true; changed = true;
} }
if (newSettings.ha_number_mode != settings.ha_number_mode) {
changed = true;
}
if (newSettings.entity_format != settings.entity_format) { if (newSettings.entity_format != settings.entity_format) {
changed = true; changed = true;
} }

View File

@@ -852,6 +852,15 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// this is for a specific sensor, return the value // this is for a specific sensor, return the value
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
@@ -866,6 +875,35 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from analog values
std::string AnalogSensor::get_metrics_prometheus() {
std::string result;
result.reserve(sensors_.size() * 140);
char val[10];
for (auto & sensor : sensors_) {
result += (std::string) "# HELP emsesp_" + sensor.name() + " " + sensor.name();
if (sensor.type() != AnalogType::DIGITAL_OUT && sensor.type() != AnalogType::DIGITAL_IN) {
result += (std::string) ", " + EMSdevice::uom_to_string(sensor.uom());
} else {
result += (std::string) ", boolean";
}
result += (std::string) ", readable, visible";
if (sensor.type() == AnalogType::COUNTER || sensor.type() == AnalogType::RGB || sensor.type() == AnalogType::PULSE
|| (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2)
|| (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) {
result += (std::string) ", writable";
}
result += (std::string) "\n# TYPE emsesp_" + sensor.name() + " gauge\n";
result += (std::string) "emsesp_" + sensor.name() + " ";
if (sensor.type() != AnalogType::DIGITAL_OUT && sensor.type() != AnalogType::DIGITAL_IN) {
result += (std::string) Helpers::render_value(val, sensor.value(), 2) + "\n";
} else {
result += (std::string) (sensor.value() == 0 ? "0\n" : "1\n");
}
}
return result;
}
// note we don't add the device and state classes here, as we do in the custom entity service // note we don't add the device and state classes here, as we do in the custom entity service
void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) { void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) {
output["name"] = (const char *)sensor.name(); output["name"] = (const char *)sensor.name();

View File

@@ -177,6 +177,7 @@ class AnalogSensor {
bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system); bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system);
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
void store_counters(); void store_counters();
std::string get_metrics_prometheus();
static std::vector<uint8_t> exclude_types() { static std::vector<uint8_t> exclude_types() {
return exclude_types_; return exclude_types_;
} }

View File

@@ -72,6 +72,21 @@ const char * EMSdevice::tag_to_mqtt(int8_t tag) {
return (DeviceValue::DeviceValueTAG_mqtt[tag > DeviceValue::NUM_TAGS ? 0 : tag]); return (DeviceValue::DeviceValueTAG_mqtt[tag > DeviceValue::NUM_TAGS ? 0 : tag]);
} }
uint8_t EMSdevice::tag_to_flag(const uint8_t tag) {
if (tag >= DeviceValueTAG::TAG_HC1 && tag <= DeviceValueTAG::TAG_HC8) {
return CommandFlag::CMD_FLAG_HC;
} else if (tag >= DeviceValueTAG::TAG_DHW1 && tag <= DeviceValueTAG::TAG_DHW10) {
return CommandFlag::CMD_FLAG_DHW;
} else if (tag >= DeviceValueTAG::TAG_HS1 && tag <= DeviceValueTAG::TAG_HS16) {
return CommandFlag::CMD_FLAG_HS;
} else if (tag >= DeviceValueTAG::TAG_AHS1 && tag <= DeviceValueTAG::TAG_AHS1) {
return CommandFlag::CMD_FLAG_AHS;
} else if (tag >= DeviceValueTAG::TAG_SRC1 && tag <= DeviceValueTAG::TAG_SRC16) {
return CommandFlag::CMD_FLAG_SRC;
}
return 0;
}
// convert UOM to a char string - translating only for hours/minutes/seconds // convert UOM to a char string - translating only for hours/minutes/seconds
const char * EMSdevice::uom_to_string(uint8_t uom) { const char * EMSdevice::uom_to_string(uint8_t uom) {
switch (uom) { switch (uom) {
@@ -89,7 +104,10 @@ const char * EMSdevice::uom_to_string(uint8_t uom) {
} }
} }
const char * EMSdevice::brand_to_char() { std::string EMSdevice::brand_to_char() {
if (!custom_brand().empty()) {
return custom_brand();
}
switch (brand_) { switch (brand_) {
case EMSdevice::Brand::BOSCH: case EMSdevice::Brand::BOSCH:
return F_(bosch); return F_(bosch);
@@ -313,15 +331,15 @@ uint8_t EMSdevice::decode_brand(uint8_t value) {
std::string EMSdevice::to_string() { std::string EMSdevice::to_string() {
// for devices that haven't been lookup yet, don't show all details // for devices that haven't been lookup yet, don't show all details
if (product_id_ == 0) { if (product_id_ == 0) {
return std::string(name()) + " (DeviceID:" + Helpers::hextoa(device_id_) + ")"; return name() + " (DeviceID:" + Helpers::hextoa(device_id_) + ")";
} }
if (brand_ == Brand::NO_BRAND) { if (brand_ == Brand::NO_BRAND && custom_brand().empty()) {
return std::string(name()) + " (DeviceID:" + Helpers::hextoa(device_id_) + ", ProductID:" + Helpers::itoa(product_id_) + ", Version:" + version_ + ")"; return name() + " (DeviceID:" + Helpers::hextoa(device_id_) + ", ProductID:" + Helpers::itoa(product_id_) + ", Version:" + version_ + ")";
} }
return std::string(brand_to_char()) + " " + name() + " (DeviceID:" + Helpers::hextoa(device_id_) + ", ProductID:" + Helpers::itoa(product_id_) return brand_to_char() + " " + name() + " (DeviceID:" + Helpers::hextoa(device_id_) + ", ProductID:" + Helpers::itoa(product_id_) + ", Version:" + version_
+ ", Version:" + version_ + ")"; + ")";
} }
// returns string of EMS device version and productID // returns string of EMS device version and productID
@@ -332,7 +350,7 @@ std::string EMSdevice::to_string_version() {
// returns out brand + device name // returns out brand + device name
// translated // translated
std::string EMSdevice::to_string_short() { std::string EMSdevice::to_string_short() {
if (brand_ == Brand::NO_BRAND) { if (brand_ == Brand::NO_BRAND && custom_brand().empty()) {
return std::string(device_type_2_device_name_translated()) + ": " + name(); return std::string(device_type_2_device_name_translated()) + ": " + name();
} }
@@ -650,25 +668,21 @@ void EMSdevice::add_device_value(int8_t tag, // to b
// add a new command if it has a function attached // add a new command if it has a function attached
if (has_cmd) { if (has_cmd) {
uint8_t flags = CommandFlag::ADMIN_ONLY; // executing commands require admin privileges uint8_t flags = CommandFlag::ADMIN_ONLY | tag_to_flag(tag); // executing commands require admin privileges
if (tag >= DeviceValueTAG::TAG_HC1 && tag <= DeviceValueTAG::TAG_HC8) {
flags |= CommandFlag::CMD_FLAG_HC;
} else if (tag >= DeviceValueTAG::TAG_DHW1 && tag <= DeviceValueTAG::TAG_DHW10) {
flags |= CommandFlag::CMD_FLAG_DHW;
} else if (tag >= DeviceValueTAG::TAG_HS1 && tag <= DeviceValueTAG::TAG_HS16) {
flags |= CommandFlag::CMD_FLAG_HS;
} else if (tag >= DeviceValueTAG::TAG_AHS1 && tag <= DeviceValueTAG::TAG_AHS1) {
flags |= CommandFlag::CMD_FLAG_AHS;
} else if (tag >= DeviceValueTAG::TAG_SRC1 && tag <= DeviceValueTAG::TAG_SRC16) {
flags |= CommandFlag::CMD_FLAG_SRC;
}
// add the command to our library // add the command to our library
Command::add(device_type_, device_id_, short_name, f, fullname, flags); Command::add(device_type_, device_id_, short_name, f, fullname, flags);
} }
} }
void EMSdevice::erase_device_values() {
for (auto & dv : devicevalues_) {
if (dv.has_cmd) {
Command::erase_command(device_type_, dv.short_name, tag_to_flag(dv.tag));
}
}
devicevalues_.clear();
}
// single list of options // single list of options
void EMSdevice::register_device_value(int8_t tag, void EMSdevice::register_device_value(int8_t tag,
void * value_p, void * value_p,
@@ -898,7 +912,7 @@ void EMSdevice::publish_value(void * value_p) const {
// looks up the UOM for a given key from the device value table // looks up the UOM for a given key from the device value table
std::string EMSdevice::get_value_uom(const std::string & shortname) const { std::string EMSdevice::get_value_uom(const std::string & shortname) const {
for (const auto & dv : devicevalues_) { for (const auto & dv : devicevalues_) {
if ((!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE)) && (dv.short_name == shortname)) { if ((!dv.has_state(DeviceValueState::DV_WEB_EXCLUDE)) && !strcmp(dv.short_name, shortname.c_str())) {
// ignore TIME since "minutes" is already added to the string value // ignore TIME since "minutes" is already added to the string value
if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) { if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) {
break; break;
@@ -1268,7 +1282,7 @@ void EMSdevice::setCustomizationEntity(const std::string & entity_id) {
// set the min / max // set the min / max
dv.set_custom_minmax(); dv.set_custom_minmax();
if (Mqtt::ha_enabled() && dv.short_name == FL_(seltemp)[0] && (min != dv.min || max != dv.max)) { if (Mqtt::ha_enabled() && dv.tag <= DeviceValueTAG::TAG_HC8 && !strcmp(dv.short_name, FL_(selRoomTemp)[0]) && (min != dv.min || max != dv.max)) {
set_climate_minmax(dv.tag, dv.min, dv.max); set_climate_minmax(dv.tag, dv.min, dv.max);
} }
@@ -2121,7 +2135,7 @@ void EMSdevice::mqtt_ha_entity_config_create() {
if (needs_update) { if (needs_update) {
const char * const ** mode_options = nullptr; const char * const ** mode_options = nullptr;
for (auto & d : devicevalues_) { for (const auto & d : devicevalues_) {
// make sure mode in same circuit is DeviceValueType::ENUM // make sure mode in same circuit is DeviceValueType::ENUM
if ((d.tag == dv.tag) && (d.type == DeviceValueType::ENUM) && !strcmp(d.short_name, FL_(mode)[0]) && (d.options_size > 0)) { if ((d.tag == dv.tag) && (d.type == DeviceValueType::ENUM) && !strcmp(d.short_name, FL_(mode)[0]) && (d.options_size > 0)) {
// get options // get options
@@ -2146,26 +2160,32 @@ void EMSdevice::mqtt_ha_entity_config_create() {
if (!dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && dv.has_state(DeviceValueState::DV_ACTIVE) if (!dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && dv.has_state(DeviceValueState::DV_ACTIVE)
&& !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE)) { && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE)) {
// create_device_config is only done once for the EMS device. It can added to any entity, so we take the first // create_device_config is only done once for the EMS device. It can added to any entity, so we take the first
if (Mqtt::publish_ha_sensor_config_dv(dv, name().c_str(), brand_to_char(), to_string_version().c_str(), false, create_device_config)) { if (Mqtt::publish_ha_sensor_config_dv(dv, name().c_str(), brand_to_char().c_str(), to_string_version().c_str(), false, create_device_config)) {
dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED); dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED);
create_device_config = false; // only create the main config once create_device_config = false; // only create the main config once
count++; count++;
} }
// SRC thermostats mapped to connect/src1/... always contains mode, seltemp, currtemp // SRC thermostats mapped to connect/src1/... always contains mode, selRoomTemp, currtemp
if (dv.tag >= DeviceValueTAG::TAG_SRC1 && dv.tag <= DeviceValueTAG::TAG_SRC16 && !strcmp(dv.short_name, FL_(mode)[0])) { if (dv.tag >= DeviceValueTAG::TAG_SRC1 && dv.tag <= DeviceValueTAG::TAG_SRC16 && !strcmp(dv.short_name, FL_(selRoomTemp)[0])) {
// add icon if we have one // add modes and icon if we have one
const char * icon = nullptr; const char * icon = nullptr;
for (auto & d : devicevalues_) { const char * const ** mode_options = nullptr;
if (d.tag == dv.tag && !strcmp(d.short_name, FL_(icon)[0]) && (dv.type == DeviceValueType::ENUM)) { for (const auto & d : devicevalues_) {
if ((d.tag != dv.tag) || (d.type != DeviceValueType::ENUM)) {
continue;
}
if (!strcmp(d.short_name, FL_(mode)[0]) && (d.options_size > 0)) {
mode_options = d.options;
}
if (!strcmp(d.short_name, FL_(icon)[0])) {
uint8_t val = *(uint8_t *)(d.value_p); uint8_t val = *(uint8_t *)(d.value_p);
if (val != 0 && val < d.options_size) { if (val != 0 && val < d.options_size) {
icon = d.options[val][0]; icon = d.options[val][0];
} }
break;
} }
} }
Mqtt::publish_ha_climate_config(dv, true, dv.options, false, icon); Mqtt::publish_ha_climate_config(dv, true, mode_options, false, icon);
count++; count++;
} }

View File

@@ -55,6 +55,7 @@ class EMSdevice {
static const char * tag_to_mqtt(int8_t tag); static const char * tag_to_mqtt(int8_t tag);
static uint8_t decode_brand(uint8_t value); static uint8_t decode_brand(uint8_t value);
static bool export_values(uint8_t device_type, JsonObject output, const int8_t id, const uint8_t output_target); static bool export_values(uint8_t device_type, JsonObject output, const int8_t id, const uint8_t output_target);
static uint8_t tag_to_flag(const uint8_t tag);
// non static functions // non static functions
@@ -62,7 +63,7 @@ class EMSdevice {
const char * device_type_2_device_name_translated(); // returns translated device type name const char * device_type_2_device_name_translated(); // returns translated device type name
bool has_tags(const int8_t tag) const; bool has_tags(const int8_t tag) const;
bool has_cmd(const char * cmd, const int8_t id) const; bool has_cmd(const char * cmd, const int8_t id) const;
const char * brand_to_char(); std::string brand_to_char();
std::string to_string(); std::string to_string();
std::string to_string_short(); std::string to_string_short();
std::string to_string_version(); std::string to_string_version();
@@ -124,6 +125,14 @@ class EMSdevice {
return custom_name_; return custom_name_;
} }
// set custom brand
void custom_brand(std::string const & custom_brand) {
custom_brand_ = custom_brand;
}
std::string custom_brand() const {
return custom_brand_;
}
// set device model // set device model
void model(std::string const & model) { void model(std::string const & model) {
model_ = model; model_ = model;
@@ -282,6 +291,8 @@ class EMSdevice {
int16_t min, int16_t min,
uint32_t max); uint32_t max);
void erase_device_values();
void void
register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p f); register_device_value(int8_t tag, void * value_p, uint8_t type, const char * const ** options, const char * const * name, uint8_t uom, const cmd_function_p f);
@@ -524,12 +535,13 @@ class EMSdevice {
uint8_t device_id_ = 0; uint8_t device_id_ = 0;
uint8_t product_id_ = 0; uint8_t product_id_ = 0;
char version_[6]; char version_[6];
const char * default_name_; // the fixed name the EMS model taken from the device library const char * default_name_; // the fixed name the EMS model taken from the device library
std::string custom_name_ = ""; // custom name std::string custom_name_ = ""; // custom name
std::string model_ = ""; // model, taken from the 0x01 telegram. see process_deviceName() std::string custom_brand_ = ""; // custom brand
uint8_t flags_ = 0; std::string model_ = ""; // model, taken from the 0x01 telegram. see process_deviceName()
uint8_t brand_ = Brand::NO_BRAND; uint8_t flags_ = 0;
bool active_ = true; uint8_t brand_ = Brand::NO_BRAND;
bool active_ = true;
bool ha_config_done_ = false; bool ha_config_done_ = false;
bool has_update_ = false; bool has_update_ = false;

View File

@@ -1310,6 +1310,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
if (product_id == 0 || (*it)->product_id() != 0) { // update only with valid product_id if (product_id == 0 || (*it)->product_id() != 0) { // update only with valid product_id
return true; return true;
} }
(*it)->erase_device_values();
emsdevices.erase(it); // erase the old device without product_id and re detect emsdevices.erase(it); // erase the old device without product_id and re detect
break; break;
} }
@@ -1450,6 +1451,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const
if ((e.device_id == device_id) && (e.product_id == product_id)) { if ((e.device_id == device_id) && (e.product_id == product_id)) {
LOG_DEBUG("Have customizations for %s with deviceID 0x%02X productID %d", e.custom_name.c_str(), device_id, product_id); LOG_DEBUG("Have customizations for %s with deviceID 0x%02X productID %d", e.custom_name.c_str(), device_id, product_id);
emsdevices.back()->custom_name(e.custom_name); emsdevices.back()->custom_name(e.custom_name);
emsdevices.back()->custom_brand(e.custom_brand);
break; break;
} }
} }
@@ -1760,7 +1762,7 @@ void EMSESP::start() {
nvs_.begin("ems-esp", false, "nvs"); // fallback to small nvs nvs_.begin("ems-esp", false, "nvs"); // fallback to small nvs
} }
LOG_DEBUG("NVS device information: %s", system_.getBBQKeesGatewayDetails().isEmpty() ? "not set" : system_.getBBQKeesGatewayDetails().c_str()); LOG_DEBUG("Fuse device information: %s", system_.getBBQKeesGatewayDetails().isEmpty() ? "not set" : system_.getBBQKeesGatewayDetails().c_str());
webSettingsService.begin(); // load EMS-ESP Application settings webSettingsService.begin(); // load EMS-ESP Application settings

View File

@@ -303,7 +303,7 @@ MAKE_ENUM(enum_comfort, FL_(hot), FL_(eco), FL_(intelligent))
MAKE_ENUM(enum_comfort1, FL_(high_comfort), FL_(eco)) MAKE_ENUM(enum_comfort1, FL_(high_comfort), FL_(eco))
MAKE_ENUM(enum_comfort2, FL_(eco), FL_(high_comfort)) MAKE_ENUM(enum_comfort2, FL_(eco), FL_(high_comfort))
MAKE_ENUM(enum_flow, FL_(off), FL_(flow), FL_(bufferedflow), FL_(buffer), FL_(layeredbuffer)) MAKE_ENUM(enum_flow, FL_(off), FL_(flow), FL_(bufferedflow), FL_(buffer), FL_(layeredbuffer))
MAKE_ENUM(enum_reset, FL_(dash), FL_(maintenance), FL_(error), FL_(history), FL_(message)) MAKE_ENUM(enum_reset, FL_(dash), FL_(maintenance), FL_(error), FL_(history), FL_(message), FL_(hp_error), FL_(burn_starts), FL_(factory))
MAKE_ENUM(enum_maxHeat, FL_(0kW), FL_(2kW), FL_(3kW), FL_(4kW), FL_(6kW), FL_(9kW)) MAKE_ENUM(enum_maxHeat, FL_(0kW), FL_(2kW), FL_(3kW), FL_(4kW), FL_(6kW), FL_(9kW))
MAKE_ENUM(enum_maxHeat1, FL_(0kW), FL_(3kW), FL_(6kW), FL_(9kW)) MAKE_ENUM(enum_maxHeat1, FL_(0kW), FL_(3kW), FL_(6kW), FL_(9kW))
MAKE_ENUM(enum_maxHeat2, FL_(3kW), FL_(6kW), FL_(9kW)) MAKE_ENUM(enum_maxHeat2, FL_(3kW), FL_(6kW), FL_(9kW))

View File

@@ -197,6 +197,9 @@ MAKE_WORD_TRANSLATION(lower, "lower", "niedriger", "lager", "lägre", "mniejszy"
MAKE_WORD_TRANSLATION(error, "error", "Fehler", "error", "Fel", "błąd", "feil", "erreur", "Hata", "errore", "error", "chyba") MAKE_WORD_TRANSLATION(error, "error", "Fehler", "error", "Fel", "błąd", "feil", "erreur", "Hata", "errore", "error", "chyba")
MAKE_WORD_TRANSLATION(history, "history", "Fehlerspeicher", "geschiedenis", "historik", "historia", "historikk", "historique", "geçmiş", "storico", "história", "historie") MAKE_WORD_TRANSLATION(history, "history", "Fehlerspeicher", "geschiedenis", "historik", "historia", "historikk", "historique", "geçmiş", "storico", "história", "historie")
MAKE_WORD_TRANSLATION(message, "message", "Meldung", "melding", "meddelande", "komunikat", "melding", "message", "mesajı", "messaggio", "správa", "zpráva") MAKE_WORD_TRANSLATION(message, "message", "Meldung", "melding", "meddelande", "komunikat", "melding", "message", "mesajı", "messaggio", "správa", "zpráva")
MAKE_WORD_TRANSLATION(hp_error, "hp error", "WP Fehler", "hp error", "hp Fel", "hp błąd", "hp feil", "hp erreur", "hp Hata", "hp errore", "hp error", "hp chyba")
MAKE_WORD_TRANSLATION(factory, "factory", "werkseinst.", "", "", "", "", "", "", "", "", "")
MAKE_WORD_TRANSLATION(burn_starts, "burner starts", "Brennerstarts", "", "", "", "", "", "", "", "", "")
MAKE_WORD_TRANSLATION(na, "n/a", "n/a", "n/a", "n/a", "nd.", "n/a", "n/c", "mevcut değil", "n/a", "n/a", "n/a") MAKE_WORD_TRANSLATION(na, "n/a", "n/a", "n/a", "n/a", "nd.", "n/a", "n/c", "mevcut değil", "n/a", "n/a", "n/a")
MAKE_WORD_TRANSLATION(inverted, "inverted", "invertiert", "omgekeerd", "inverterad", "odwrócony", "invertert", "inversé", "ters", "invertito", "invertovaný", "invertovaný") MAKE_WORD_TRANSLATION(inverted, "inverted", "invertiert", "omgekeerd", "inverterad", "odwrócony", "invertert", "inversé", "ters", "invertito", "invertovaný", "invertovaný")
@@ -424,6 +427,10 @@ MAKE_TRANSLATION(pumpOnTemp, "pumpontemp", "pump logic temperature", "Pumpenlogi
MAKE_TRANSLATION(headertemp, "headertemp", "low loss header", "Hydr. Weiche", "open verdeler", "Fördelare", "sprzęgło hydrauliczne", "lav tap header", "bouteille de déc. hydr.", "isı bloğu gidiş suyu sıc.", "comp. idr.", "nízkostratová hlavica", "hydraulický oddělovač") MAKE_TRANSLATION(headertemp, "headertemp", "low loss header", "Hydr. Weiche", "open verdeler", "Fördelare", "sprzęgło hydrauliczne", "lav tap header", "bouteille de déc. hydr.", "isı bloğu gidiş suyu sıc.", "comp. idr.", "nízkostratová hlavica", "hydraulický oddělovač")
MAKE_TRANSLATION(heatblock, "heatblock", "heating block", "Wärmezelle", "Aanvoertemp. warmtecel", "Värmeblock", "blok grzewczy", "varmeblokk", "départ corps de chauffe", "Hid.denge kabı sıcaklığı", "mandata scamb. pr.", "vykurovací blok", "blok topení") MAKE_TRANSLATION(heatblock, "heatblock", "heating block", "Wärmezelle", "Aanvoertemp. warmtecel", "Värmeblock", "blok grzewczy", "varmeblokk", "départ corps de chauffe", "Hid.denge kabı sıcaklığı", "mandata scamb. pr.", "vykurovací blok", "blok topení")
MAKE_TRANSLATION(pumpKickHour, "pumpkickhour", "pump kick hour", "Stunde Pumpkick")
MAKE_TRANSLATION(pumpKickDay, "pumpkickday", "pump kick day", "Tag Pumpkick")
MAKE_TRANSLATION(pumpKickDelay, "pumpkickdelay", "pump kick delay", "Pause vor Pumpkick")
MAKE_TRANSLATION(curveOn, "curveon", "heatingcurve on", "Heizkurve an", "stookkromme aan", "Värmekurva På", "krzywa grzewcza włączona", "varmekurve på", "courbe de chauffage activée", "ısıtma eğrisi açık", "curva di riscaldamento attiva", "vykurovacia krivka zapnutá", "topná křivka zapnutá") MAKE_TRANSLATION(curveOn, "curveon", "heatingcurve on", "Heizkurve an", "stookkromme aan", "Värmekurva På", "krzywa grzewcza włączona", "varmekurve på", "courbe de chauffage activée", "ısıtma eğrisi açık", "curva di riscaldamento attiva", "vykurovacia krivka zapnutá", "topná křivka zapnutá")
MAKE_TRANSLATION(curveBase, "curvebase", "heatingcurve base", "Heizkurve Basis", "stookkromme basis", "Värmekurva Bas", "podstawa krzywej grzewczej", "varmekurve basis", "base de courbe de chauffage", "ısıtma eğrisi tabanı", "base curva di riscaldamento", "základňa vykurovacej krivky", "základ topné křivky") MAKE_TRANSLATION(curveBase, "curvebase", "heatingcurve base", "Heizkurve Basis", "stookkromme basis", "Värmekurva Bas", "podstawa krzywej grzewczej", "varmekurve basis", "base de courbe de chauffage", "ısıtma eğrisi tabanı", "base curva di riscaldamento", "základňa vykurovacej krivky", "základ topné křivky")
MAKE_TRANSLATION(curveEnd, "curveend", "heatingcurve end", "Heizkurve Ende", "stookkromme einde", "Värmekurva Slut", "koniec krzywej grzewczej", "varmekurve slutt", "fin de courbe de chauffage", "ısıtma eğrisi sonu", "fine curva di riscaldamento", "koniec vykurovacej krivky", "konec topné křivky") MAKE_TRANSLATION(curveEnd, "curveend", "heatingcurve end", "Heizkurve Ende", "stookkromme einde", "Värmekurva Slut", "koniec krzywej grzewczej", "varmekurve slutt", "fin de courbe de chauffage", "ısıtma eğrisi sonu", "fine curva di riscaldamento", "koniec vykurovacej krivky", "konec topné křivky")
@@ -773,6 +780,7 @@ MAKE_TRANSLATION(comforttemp, "comforttemp", "comfort temperature", "Komforttemp
MAKE_TRANSLATION(summertemp, "summertemp", "summer temperature", "Sommertemperatur", "Zomertemperatuur", "Sommartemperatur", "temperatura przełączania lato/zima", "Sommertemperatur", "température été", "yaz sıcaklığı", "temperatura estiva", "letná teplota", "letní teplota") MAKE_TRANSLATION(summertemp, "summertemp", "summer temperature", "Sommertemperatur", "Zomertemperatuur", "Sommartemperatur", "temperatura przełączania lato/zima", "Sommertemperatur", "température été", "yaz sıcaklığı", "temperatura estiva", "letná teplota", "letní teplota")
MAKE_TRANSLATION(designtemp, "designtemp", "design temperature", "Auslegungstemperatur", "Ontwerptemperatuur", "Design-temperatur", "temperatura projektowa", "designtemperatur", "température conception", "özel sıcaklık", "temperatura predefinita", "návrhová teplota", "dimenzovaná teplota") MAKE_TRANSLATION(designtemp, "designtemp", "design temperature", "Auslegungstemperatur", "Ontwerptemperatuur", "Design-temperatur", "temperatura projektowa", "designtemperatur", "température conception", "özel sıcaklık", "temperatura predefinita", "návrhová teplota", "dimenzovaná teplota")
MAKE_TRANSLATION(offsettemp, "offsettemp", "offset temperature", "Temperaturanhebung", "Temperatuur offset", "Temperaturkorrigering", "korekta temperatury", "temperaturkorrigering", "température offset", "artış sıcaklığı", "aumento della temperatura", "offsetová teplota", "offset teploty") MAKE_TRANSLATION(offsettemp, "offsettemp", "offset temperature", "Temperaturanhebung", "Temperatuur offset", "Temperaturkorrigering", "korekta temperatury", "temperaturkorrigering", "température offset", "artış sıcaklığı", "aumento della temperatura", "offsetová teplota", "offset teploty")
MAKE_TRANSLATION(baseflowtemp, "baseflowtemp", "base flow temperature", "Basis Vorlauftemperatur") // ToDo translate
MAKE_TRANSLATION(minflowtemp, "minflowtemp", "min flow temperature", "min. Vorlauftemperatur", "Minimale aanvoertemperatuur", "Min. Flödestemperatur", "minimalna temperatura zasilania", "min. turtemperatur", "température min. flux", "minimun akış sıcaklığı", "temperatura minima di mandata", "min. výstupná teplota", "vytápění minimální teplota") MAKE_TRANSLATION(minflowtemp, "minflowtemp", "min flow temperature", "min. Vorlauftemperatur", "Minimale aanvoertemperatuur", "Min. Flödestemperatur", "minimalna temperatura zasilania", "min. turtemperatur", "température min. flux", "minimun akış sıcaklığı", "temperatura minima di mandata", "min. výstupná teplota", "vytápění minimální teplota")
MAKE_TRANSLATION(maxflowtemp, "maxflowtemp", "max flow temperature", "max. Vorlauftemperatur", "Maximale aanvoertemperatuur", "Max. Flödestemperatur", "maksymalna temperatura zasilania", "maks turtemperatur", "température max flux", "maksimum akış sıcaklığı", "temperatura massima di mandata", "maximálna teplota prívodu", "vytápění maximální teplota") MAKE_TRANSLATION(maxflowtemp, "maxflowtemp", "max flow temperature", "max. Vorlauftemperatur", "Maximale aanvoertemperatuur", "Max. Flödestemperatur", "maksymalna temperatura zasilania", "maks turtemperatur", "température max flux", "maksimum akış sıcaklığı", "temperatura massima di mandata", "maximálna teplota prívodu", "vytápění maximální teplota")
MAKE_TRANSLATION(roominfluence, "roominfluence", "room influence", "Raumeinfluss", "Ruimteinvloed", "Rumspåverkan", "wpływ pomieszczenia", "rominnflytelse", "influence pièce", "oda etkisi", "influenza della camera", "vplyv miestnosti", "vliv prostoru") MAKE_TRANSLATION(roominfluence, "roominfluence", "room influence", "Raumeinfluss", "Ruimteinvloed", "Rumspåverkan", "wpływ pomieszczenia", "rominnflytelse", "influence pièce", "oda etkisi", "influenza della camera", "vplyv miestnosti", "vliv prostoru")

View File

@@ -1545,9 +1545,7 @@ void Mqtt::add_ha_dev_section(JsonObject doc, const char * name, const bool crea
// add mf (manufacturer/brand), mdl (model), sw (software version) and via_device // add mf (manufacturer/brand), mdl (model), sw (software version) and via_device
dev_json["mf"] = brand != nullptr ? brand : "EMS-ESP"; dev_json["mf"] = brand != nullptr ? brand : "EMS-ESP";
if (model != nullptr) { dev_json["mdl"] = model != nullptr ? model : "EMS-ESP";
dev_json["mdl"] = model;
}
dev_json["sw"] = version != nullptr ? version : "v" + std::string(EMSESP_APP_VERSION); dev_json["sw"] = version != nullptr ? version : "v" + std::string(EMSESP_APP_VERSION);
dev_json["via_device"] = Mqtt::basename(); dev_json["via_device"] = Mqtt::basename();
} }

View File

@@ -2952,7 +2952,6 @@ void System::set_valid_system_gpios() {
// excluded: // excluded:
// GPIO3, GPIO45 - GPIO46 = strapping pins // GPIO3, GPIO45 - GPIO46 = strapping pins
// GPIO26 - GPIO32 = SPI flash and PSRAM and not recommended // GPIO26 - GPIO32 = SPI flash and PSRAM and not recommended
// GPIO33 - GPIO37 = Octal flash/PSRAM
// GPIO19 - GPIO20 = USB-JTAG // GPIO19 - GPIO20 = USB-JTAG
// GPIO22 - GPIO25 = don't exist // GPIO22 - GPIO25 = don't exist
// //
@@ -2960,7 +2959,12 @@ void System::set_valid_system_gpios() {
// GPIO11 - GPIO19 = ADC analog input only pins // GPIO11 - GPIO19 = ADC analog input only pins
// GPIO47 - GPIO48 = valid on a Wemos S3 // GPIO47 - GPIO48 = valid on a Wemos S3
// GPIO8 = used by Liligo S3 board profile for Rx // GPIO8 = used by Liligo S3 board profile for Rx
valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 33-37, 19-20, 22-25"); if (ESP.getPsramSize() > 0) {
// GPIO33 - GPIO37 = Octal flash/PSRAM
valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 33-37, 19-20, 22-25");
} else {
valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 19-20, 22-25");
}
#elif CONFIG_IDF_TARGET_ESP32 #elif CONFIG_IDF_TARGET_ESP32
// https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html

View File

@@ -400,6 +400,15 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// this is for a specific sensor // this is for a specific sensor
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
@@ -414,6 +423,21 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from temperature values
std::string TemperatureSensor::get_metrics_prometheus() {
std::string result;
result.reserve(sensors_.size() * 120);
char val[10];
for (auto & sensor : sensors_) {
result += (std::string) "# HELP emsesp_" + sensor.name() + " " + sensor.name() + ", "
+ EMSdevice::uom_to_string(EMSESP::system_.fahrenheit() ? DeviceValueUOM::FAHRENHEIT : DeviceValueUOM::DEGREES) + ", readable, visible\n";
result += (std::string) "# TYPE emsesp_" + sensor.name() + " gauge\n";
result +=
(std::string) "emsesp_" + sensor.name() + " " + Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0) + "\n";
}
return result;
}
// note we don't add the device and state classes here, as we do in the custom entity service // note we don't add the device and state classes here, as we do in the custom entity service
void TemperatureSensor::get_value_json(JsonObject output, const Sensor & sensor) { void TemperatureSensor::get_value_json(JsonObject output, const Sensor & sensor) {
output["id"] = sensor.id(); output["id"] = sensor.id();

View File

@@ -96,6 +96,8 @@ class TemperatureSensor {
bool updated_values(); bool updated_values();
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
std::string get_metrics_prometheus();
// return back reference to the sensor list, used by other classes // return back reference to the sensor list, used by other classes
std::vector<Sensor, AllocatorPSRAM<Sensor>> sensors() const { std::vector<Sensor, AllocatorPSRAM<Sensor>> sensors() const {
return sensors_; return sensors_;

View File

@@ -65,6 +65,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_telegram_type(0xE6, "UBAParametersPlus", true, MAKE_PF_CB(process_UBAParametersPlus)); register_telegram_type(0xE6, "UBAParametersPlus", true, MAKE_PF_CB(process_UBAParametersPlus));
register_telegram_type(0xE9, "UBAMonitorWWPlus", false, MAKE_PF_CB(process_UBAMonitorWWPlus)); register_telegram_type(0xE9, "UBAMonitorWWPlus", false, MAKE_PF_CB(process_UBAMonitorWWPlus));
register_telegram_type(0xEA, "UBAParameterWWPlus", true, MAKE_PF_CB(process_UBAParameterWWPlus)); register_telegram_type(0xEA, "UBAParameterWWPlus", true, MAKE_PF_CB(process_UBAParameterWWPlus));
register_telegram_type(0xEB, "PumpKick", true, MAKE_PF_CB(process_PumpKick));
register_telegram_type(0x28, "WeatherComp", true, MAKE_PF_CB(process_WeatherComp)); register_telegram_type(0x28, "WeatherComp", true, MAKE_PF_CB(process_WeatherComp));
register_telegram_type(0x2E0, "UBASetPoints", false, MAKE_PF_CB(process_UBASetPoints2)); register_telegram_type(0x2E0, "UBASetPoints", false, MAKE_PF_CB(process_UBASetPoints2));
register_telegram_type(0x2CC, "HPPressure", true, MAKE_PF_CB(process_HpPressure)); register_telegram_type(0x2CC, "HPPressure", true, MAKE_PF_CB(process_HpPressure));
@@ -355,6 +356,24 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pc1On_, DeviceValueType::BOOL, FL_(pc1On), DeviceValueUOM::NONE); register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pc1On_, DeviceValueType::BOOL, FL_(pc1On), DeviceValueUOM::NONE);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pc1Rate_, DeviceValueType::UINT8, FL_(pc1Rate), DeviceValueUOM::PERCENT); register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &pc1Rate_, DeviceValueType::UINT8, FL_(pc1Rate), DeviceValueUOM::PERCENT);
register_device_value(
DeviceValueTAG::TAG_DEVICE_DATA, &pumpKickHour_, DeviceValueType::UINT8, FL_(pumpKickHour), DeviceValueUOM::HOURS, MAKE_CF_CB(set_pumpKickHour), 0, 23);
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&pumpKickDay_,
DeviceValueType::ENUM,
FL_(enum_dayOfWeek),
FL_(pumpKickDay),
DeviceValueUOM::NONE,
MAKE_CF_CB(set_pumpKickDay));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&pumpKickDelay_,
DeviceValueType::UINT16,
FL_(pumpKickDelay),
DeviceValueUOM::MINUTES,
MAKE_CF_CB(set_pumpKickDelay),
0,
32767);
/* /*
* Hybrid heatpump with telegram 0xBB is readable and writeable in boiler and thermostat * Hybrid heatpump with telegram 0xBB is readable and writeable in boiler and thermostat
* thermostat always overwrites settings in boiler * thermostat always overwrites settings in boiler
@@ -2246,6 +2265,13 @@ void Boiler::process_HpPowerLimit(std::shared_ptr<const Telegram> telegram) {
has_update(telegram, hpPowerLimit_, 0); has_update(telegram, hpPowerLimit_, 0);
} }
// 0x0EB
void Boiler::process_PumpKick(std::shared_ptr<const Telegram> telegram) {
has_update(telegram, pumpKickHour_, 0);
has_enumupdate(telegram, pumpKickDay_, 1, 1); // 1-mo, ...
has_update(telegram, pumpKickDelay_, 2);
}
// Boiler(0x08) -B-> All(0x00), ?(0x2E), data: 00 00 1C CE 00 00 05 E8 00 00 00 18 00 00 00 02 // Boiler(0x08) -B-> All(0x00), ?(0x2E), data: 00 00 1C CE 00 00 05 E8 00 00 00 18 00 00 00 02
void Boiler::process_Meters(std::shared_ptr<const Telegram> telegram) { void Boiler::process_Meters(std::shared_ptr<const Telegram> telegram) {
has_update(telegram, gasMeterHeat_, 0); has_update(telegram, gasMeterHeat_, 0);
@@ -2979,25 +3005,26 @@ bool Boiler::set_reset(const char * value, const int8_t id) {
} }
if (num == 0) { if (num == 0) {
return true; // dash return true; // dash
} else if (num == 1) { } else if (num == 1) { // Reset boiler maintenance message;
// LOG_INFO("Reset boiler maintenance message");
write_command(0x05, 8, 0xFF, 0x1C); write_command(0x05, 8, 0xFF, 0x1C);
return true; return true;
} else if (num == 2) { } else if (num == 2) { // Reset boiler error message;
// LOG_INFO("Reset boiler error message"); write_command(0x05, 0, 0x5A);
write_command(0x05, 0, 0x5A); // error reset
return true; return true;
} else if (num == 3) { } else if (num == 3) { // Reset boiler history
// LOG_INFO("Reset boiler history"); write_command(0x05, 42, 0x01);
write_command(0x05, 42, 0x01); // clear history
return true; return true;
} else if (num == 4) { } else if (num == 4) {
// LOG_INFO("Reset boiler message"); write_command(0x05, 8, 0xFF); // reset messages, same as maintenance reset (1)
write_command(0x05, 8, 0xFF); // same as maintenance
return true; return true;
} else if (num == 5) { } else if (num == 5) { // reset Heatpump errors
// LOG_INFO("Factory Reset"); write_command(0x05, 50, 0xFF);
return true;
} else if (num == 6) { // reset burner starts
write_command(0x05, 2, 165);
return true;
} else if (num == 7) { // factory reset
write_command(0x05, 6, 154); write_command(0x05, 6, 154);
return true; return true;
} }
@@ -3628,4 +3655,32 @@ bool Boiler::set_shutdown(const char * value, const int8_t id) {
return false; return false;
} }
bool Boiler::set_pumpKickHour(const char * value, const int8_t id) {
int v;
if (Helpers::value2number(value, v, 0, 23)) {
write_command(0xEB, 0, v, 0xEB);
return true;
}
return false;
}
bool Boiler::set_pumpKickDay(const char * value, const int8_t id) {
uint8_t v;
if (Helpers::value2enum(value, v, FL_(enum_dayOfWeek))) {
write_command(0xEB, 1, v + 1, 0xEB);
return true;
}
return false;
}
bool Boiler::set_pumpKickDelay(const char * value, const int8_t id) {
int v;
if (Helpers::value2number(value, v, 0, 32767)) {
uint8_t data[2] = {(uint8_t)(v >> 8), (uint8_t)v};
write_command(0xEB, 2, data, 2, 0xEB);
return true;
}
return false;
}
} // namespace emsesp } // namespace emsesp

View File

@@ -319,6 +319,9 @@ class Boiler : public EMSdevice {
int16_t pc1Flow_; int16_t pc1Flow_;
uint8_t pc1Rate_; uint8_t pc1Rate_;
uint8_t pc1On_; uint8_t pc1On_;
uint8_t pumpKickHour_; // hour
uint8_t pumpKickDay_; // day 1=mo
uint16_t pumpKickDelay_; // delay minutes after pump running
// HIU // HIU
// uint16_t cwFlowRate_; // cold water flow rate *10 // uint16_t cwFlowRate_; // cold water flow rate *10
@@ -397,6 +400,7 @@ class Boiler : public EMSdevice {
void process_HpFan(std::shared_ptr<const Telegram> telegram); void process_HpFan(std::shared_ptr<const Telegram> telegram);
void process_HpPower2(std::shared_ptr<const Telegram> telegram); void process_HpPower2(std::shared_ptr<const Telegram> telegram);
void process_HpPowerLimit(std::shared_ptr<const Telegram> telegram); void process_HpPowerLimit(std::shared_ptr<const Telegram> telegram);
void process_PumpKick(std::shared_ptr<const Telegram> telegram);
void process_Meters(std::shared_ptr<const Telegram> telegram); void process_Meters(std::shared_ptr<const Telegram> telegram);
void process_Energy(std::shared_ptr<const Telegram> telegram); void process_Energy(std::shared_ptr<const Telegram> telegram);
@@ -602,6 +606,10 @@ class Boiler : public EMSdevice {
bool set_nrgHeat(const char * value, const int8_t id); bool set_nrgHeat(const char * value, const int8_t id);
bool set_nrgWw(const char * value, const int8_t id); bool set_nrgWw(const char * value, const int8_t id);
bool set_nomPower(const char * value, const int8_t id); bool set_nomPower(const char * value, const int8_t id);
bool set_pumpKickHour(const char * value, const int8_t id);
bool set_pumpKickDay(const char * value, const int8_t id);
bool set_pumpKickDelay(const char * value, const int8_t id);
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -129,15 +129,20 @@ void Connect::process_roomThermostat(std::shared_ptr<const Telegram> telegram) {
} }
has_update(telegram, rc->temp_, 0); has_update(telegram, rc->temp_, 0);
has_update(telegram, rc->humidity_, 2); // could show -3 if not set has_update(telegram, rc->humidity_, 2); // could show -3 if not set
has_update(telegram, rc->seltemp_, 3); // make sure we have read mode and icon, needed for ha climate
if (!Mqtt::ha_enabled() || (Helpers::hasValue(rc->mode_) && Helpers::hasValue(rc->icon_))) {
has_update(telegram, rc->seltemp_, 3);
}
// calculate dew temperature // calculate dew temperature
const float k2 = 17.62; if (rc->humidity_ >= 0 && rc->humidity_ <= 100) {
const float k3 = 243.12; const float k2 = 17.62;
const float t = (float)rc->temp_ / 10; const float k3 = 243.12;
const float h = (float)rc->humidity_ / 100; const float t = (float)rc->temp_ / 10;
int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h))); const float h = (float)rc->humidity_ / 100;
has_update(rc->dewtemp_, dt); int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h)));
has_update(rc->dewtemp_, dt);
}
} }
// gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in offset 0 // gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in offset 0
@@ -206,12 +211,13 @@ bool Connect::set_mode(const char * value, const int8_t id) {
return false; return false;
} }
uint8_t v; uint8_t v;
if (Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) { if (!Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) {
// if (Helpers::value2enum(value, v, FL_(enum_mode8))) { if (!Helpers::value2enum(value, v, FL_(enum_mode_ha), {3, 1, 0})) {
write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted return false;
return true; }
} }
return false; write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted
return true;
} }
bool Connect::set_seltemp(const char * value, const int8_t id) { bool Connect::set_seltemp(const char * value, const int8_t id) {
@@ -221,8 +227,9 @@ bool Connect::set_seltemp(const char * value, const int8_t id) {
} }
float v; float v;
if (Helpers::value2float(value, v)) { if (Helpers::value2float(value, v)) {
// write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); // depends on mode, auto (2 for enum_mode2, 0 for enum_mode8) set in offset 1
write_command(0xBB5 + rc->room(), rc->mode_ == 0 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2));
// write_command(0xBB5 + rc->room(), rc->mode_ == 0 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2));
return true; return true;
} }
return false; return false;

View File

@@ -1101,6 +1101,8 @@ void Thermostat::process_JunkersWW(std::shared_ptr<const Telegram> telegram) {
void Thermostat::process_JunkersDisp(std::shared_ptr<const Telegram> telegram) { void Thermostat::process_JunkersDisp(std::shared_ptr<const Telegram> telegram) {
has_enumupdate(telegram, ibaMainDisplay_, 1, 1); has_enumupdate(telegram, ibaMainDisplay_, 1, 1);
has_update(telegram, ibaLanguage_, 3); has_update(telegram, ibaLanguage_, 3);
has_update(telegram, ibaMinExtTemperature_, 16);
has_update(telegram, ibaBuildingType_, 17); // percent /10
} }
// type 0x02A5 - data from Worchester CRF200 // type 0x02A5 - data from Worchester CRF200
@@ -1250,12 +1252,12 @@ void Thermostat::process_RC300Summer(std::shared_ptr<const Telegram> telegram) {
if (hc->heatingtype != 3) { if (hc->heatingtype != 3) {
has_update(telegram, hc->designtemp, 4); has_update(telegram, hc->designtemp, 4);
has_update(telegram, hc->minflowtemp, model() == EMSdevice::EMS_DEVICE_FLAG_BC400 ? 13 : 8);
} else { } else {
has_update(telegram, hc->designtemp, 5); has_update(telegram, hc->designtemp, 5);
has_update(telegram, hc->minflowtemp, 8);
} }
// minflowtemp could be in 8 or 13 #2969
has_update(telegram, hc->minflowtemp, 13);
has_update(telegram, hc->baseflowtemp, 8);
has_update(telegram, hc->fastHeatup, 10); has_update(telegram, hc->fastHeatup, 10);
has_update(telegram, hc->comfortPointOffset, 11); has_update(telegram, hc->comfortPointOffset, 11);
has_update(telegram, hc->comfortPointTemp, 12); has_update(telegram, hc->comfortPointTemp, 12);
@@ -2025,6 +2027,8 @@ bool Thermostat::set_minexttemp(const char * value, const int8_t id) {
write_command(0x241, 10, mt, 0x241); write_command(0x241, 10, mt, 0x241);
} else if (isRC300()) { } else if (isRC300()) {
write_command(0x240, 10, mt, 0x240); write_command(0x240, 10, mt, 0x240);
} else if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) {
write_command(0x110, 16, mt, 0x110);
} else { } else {
write_command(EMS_TYPE_IBASettings, 5, mt, EMS_TYPE_IBASettings); write_command(EMS_TYPE_IBASettings, 5, mt, EMS_TYPE_IBASettings);
} }
@@ -2181,6 +2185,14 @@ bool Thermostat::set_remotehum(const char * value, const int8_t id) {
// 0xA5/0xA7 - Set the building settings // 0xA5/0xA7 - Set the building settings
bool Thermostat::set_building(const char * value, const int8_t id) { bool Thermostat::set_building(const char * value, const int8_t id) {
if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) {
int i;
if (Helpers::value2number(value, i, 0, 100)) {
write_command(0x110, 17, i / 10, 0x110);
return true;
}
return false;
}
uint8_t bd; uint8_t bd;
if (!Helpers::value2enum(value, bd, FL_(enum_ibaBuildingType))) { if (!Helpers::value2enum(value, bd, FL_(enum_ibaBuildingType))) {
return false; return false;
@@ -2326,6 +2338,7 @@ bool Thermostat::set_control(const char * value, const int8_t id) {
// 1-FB10, 2-FB100 // 1-FB10, 2-FB100
if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS && !has_flags(EMSdevice::EMS_DEVICE_FLAG_JUNKERS_OLD)) { if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS && !has_flags(EMSdevice::EMS_DEVICE_FLAG_JUNKERS_OLD)) {
if (Helpers::value2enum(value, ctrl, FL_(enum_j_control))) { if (Helpers::value2enum(value, ctrl, FL_(enum_j_control))) {
hc->control = ctrl; // set in advance, dont wait for verify
write_command(set_typeids[hc->hc()], 1, ctrl); write_command(set_typeids[hc->hc()], 1, ctrl);
return true; return true;
} }
@@ -2374,6 +2387,7 @@ bool Thermostat::set_control(const char * value, const int8_t id) {
} }
} else if (Helpers::value2enum(value, ctrl, FL_(enum_control))) { } else if (Helpers::value2enum(value, ctrl, FL_(enum_control))) {
write_command(set_typeids[hc->hc()], EMS_OFFSET_RC35Set_control, ctrl); write_command(set_typeids[hc->hc()], EMS_OFFSET_RC35Set_control, ctrl);
hc->control = ctrl; // set in advance, dont wait for verify
return true; return true;
} }
@@ -4074,10 +4088,16 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co
} }
factor = 1; factor = 1;
break; break;
case HeatingCircuit::Mode::BASEFLOW:
set_typeid = summer_typeids[hc->hc()];
validate_typeid = set_typeid;
offset = 8;
factor = 1;
break;
case HeatingCircuit::Mode::MINFLOW: case HeatingCircuit::Mode::MINFLOW:
set_typeid = summer_typeids[hc->hc()]; set_typeid = summer_typeids[hc->hc()];
validate_typeid = set_typeid; validate_typeid = set_typeid;
offset = hc->heatingtype != 3 && model == EMS_DEVICE_FLAG_BC400 ? 13 : 8; offset = 13;
factor = 1; factor = 1;
break; break;
case HeatingCircuit::Mode::MAXFLOW: case HeatingCircuit::Mode::MAXFLOW:
@@ -4730,6 +4750,19 @@ void Thermostat::register_device_values() {
DeviceValueUOM::NONE, DeviceValueUOM::NONE,
MAKE_CF_CB(set_language)); MAKE_CF_CB(set_language));
} }
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&ibaBuildingType_,
DeviceValueType::UINT8,
DeviceValueNumOp::DV_NUMOP_MUL10,
FL_(ibaBuildingType),
DeviceValueUOM::PERCENT,
MAKE_CF_CB(set_building));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&ibaMinExtTemperature_,
DeviceValueType::INT8,
FL_(ibaMinExtTemperature),
DeviceValueUOM::DEGREES,
MAKE_CF_CB(set_minexttemp));
register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, register_device_value(DeviceValueTAG::TAG_DEVICE_DATA,
&hybridStrategy_, &hybridStrategy_,
DeviceValueType::ENUM, DeviceValueType::ENUM,
@@ -4880,6 +4913,7 @@ void Thermostat::register_device_values_hc(std::shared_ptr<Thermostat::HeatingCi
register_device_value(tag, &hc->summertemp, DeviceValueType::UINT8, FL_(summertemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_summertemp), 10, 30); register_device_value(tag, &hc->summertemp, DeviceValueType::UINT8, FL_(summertemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_summertemp), 10, 30);
register_device_value(tag, &hc->designtemp, DeviceValueType::UINT8, FL_(designtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_designtemp)); register_device_value(tag, &hc->designtemp, DeviceValueType::UINT8, FL_(designtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_designtemp));
register_device_value(tag, &hc->offsettemp, DeviceValueType::INT8, FL_(offsettemp), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_offsettemp)); register_device_value(tag, &hc->offsettemp, DeviceValueType::INT8, FL_(offsettemp), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_offsettemp));
register_device_value(tag, &hc->baseflowtemp, DeviceValueType::UINT8, FL_(baseflowtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_baseflowtemp));
register_device_value(tag, &hc->minflowtemp, DeviceValueType::UINT8, FL_(minflowtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_minflowtemp)); register_device_value(tag, &hc->minflowtemp, DeviceValueType::UINT8, FL_(minflowtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_minflowtemp));
register_device_value(tag, &hc->maxflowtemp, DeviceValueType::UINT8, FL_(maxflowtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_maxflowtemp)); register_device_value(tag, &hc->maxflowtemp, DeviceValueType::UINT8, FL_(maxflowtemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_maxflowtemp));
register_device_value(tag, &hc->roominfluence, DeviceValueType::UINT8, FL_(roominfluence), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_roominfluence)); register_device_value(tag, &hc->roominfluence, DeviceValueType::UINT8, FL_(roominfluence), DeviceValueUOM::DEGREES_R, MAKE_CF_CB(set_roominfluence));

View File

@@ -50,7 +50,7 @@ class Thermostat : public EMSdevice {
uint8_t daymidtemp; uint8_t daymidtemp;
uint8_t nighttemp; uint8_t nighttemp;
uint8_t holidaytemp; uint8_t holidaytemp;
uint8_t heatingtype; // type of heating: 1 radiator, 2 convectors, 3 floors, 4 room supply uint8_t heatingtype; // type of heating: 1 radiator, 2 convectors, 3 floor, 4 room supply
uint8_t targetflowtemp; uint8_t targetflowtemp;
uint8_t summertemp; uint8_t summertemp;
int8_t nofrosttemp; // signed -20°C to +10°C int8_t nofrosttemp; // signed -20°C to +10°C
@@ -65,6 +65,7 @@ class Thermostat : public EMSdevice {
int16_t curroominfl; int16_t curroominfl;
uint8_t flowtempoffset; uint8_t flowtempoffset;
uint8_t minflowtemp; uint8_t minflowtemp;
uint8_t baseflowtemp;
uint8_t maxflowtemp; uint8_t maxflowtemp;
uint8_t reducemode; uint8_t reducemode;
uint8_t nofrostmode; uint8_t nofrostmode;
@@ -169,6 +170,7 @@ class Thermostat : public EMSdevice {
DAYMID, DAYMID,
COOLTEMP, COOLTEMP,
COOLSTART, COOLSTART,
BASEFLOW,
UNKNOWN UNKNOWN
}; };
@@ -583,6 +585,9 @@ class Thermostat : public EMSdevice {
inline bool set_minflowtemp(const char * value, const int8_t id) { inline bool set_minflowtemp(const char * value, const int8_t id) {
return set_temperature_value(value, id, HeatingCircuit::Mode::MINFLOW); return set_temperature_value(value, id, HeatingCircuit::Mode::MINFLOW);
} }
inline bool set_baseflowtemp(const char * value, const int8_t id) {
return set_temperature_value(value, id, HeatingCircuit::Mode::BASEFLOW);
}
inline bool set_roominfluence(const char * value, const int8_t id) { inline bool set_roominfluence(const char * value, const int8_t id) {
return set_temperature_value(value, id, HeatingCircuit::Mode::ROOMINFLUENCE, true); return set_temperature_value(value, id, HeatingCircuit::Mode::ROOMINFLUENCE, true);
} }

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.8.2-dev.3" #define EMSESP_APP_VERSION "3.8.2-dev.9"

View File

@@ -343,6 +343,15 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
// specific value info // specific value info
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
for (auto const & entity : *customEntityItems_) { for (auto const & entity : *customEntityItems_) {
@@ -354,6 +363,54 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd)
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from custom entities
std::string WebCustomEntityService::get_metrics_prometheus() {
std::string result;
result.reserve(customEntityItems_->size() * 140);
char val[10];
for (CustomEntityItem & entity : *customEntityItems_) {
if (entity.hide || entity.name[0] == '\0') {
continue;
}
result += (std::string) "# HELP emsesp_" + entity.name + " " + entity.name;
if (entity.uom != 0) {
result += (std::string) ", " + EMSdevice::uom_to_string(entity.uom);
}
result += (std::string) ", readable, visible" + (entity.writeable ? ", writable\n" : "\n");
result += (std::string) "# TYPE emsesp_" + entity.name + " gauge\n";
result += (std::string) "emsesp_" + entity.name + " ";
switch (entity.value_type) {
case DeviceValueType::BOOL:
result += (std::string)(entity.value == 0 ? "0" : "1");
break;
case DeviceValueType::INT8:
result += (std::string)Helpers::render_value(val, entity.factor * (int8_t)entity.value, 2);
break;
case DeviceValueType::UINT8:
result += (std::string)Helpers::render_value(val, entity.factor * (uint8_t)entity.value, 2);
break;
case DeviceValueType::INT16:
result += (std::string)Helpers::render_value(val, entity.factor * (int16_t)entity.value, 2);
break;
case DeviceValueType::UINT16:
result += (std::string)Helpers::render_value(val, entity.factor * (uint16_t)entity.value, 2);
break;
case DeviceValueType::UINT24:
case DeviceValueType::TIME:
case DeviceValueType::UINT32:
result += (std::string)Helpers::render_value(val, entity.factor * entity.value, 2);
break;
default:
if (entity.data.length() > 0) {
result += entity.data;
}
break;
}
result += (std::string) "\n";
}
return result;
}
// build the json for specific entity // build the json for specific entity
void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) { void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) {
output["name"] = (const char *)entity.name; output["name"] = (const char *)entity.name;

View File

@@ -68,6 +68,8 @@ class WebCustomEntityService : public StatefulService<WebCustomEntity> {
void show_values(JsonObject output); void show_values(JsonObject output);
void generate_value_web(JsonObject output, const bool is_dashboard = false); void generate_value_web(JsonObject output, const bool is_dashboard = false);
std::string get_metrics_prometheus();
uint8_t count_entities(); uint8_t count_entities();
void ha_reset() { void ha_reset() {
ha_configdone_ = false; ha_configdone_ = false;

View File

@@ -76,10 +76,11 @@ void WebCustomization::read(WebCustomization & customizations, JsonObject root)
// Masked entities customization and custom device name (optional) // Masked entities customization and custom device name (optional)
JsonArray masked_entitiesJson = root["masked_entities"].to<JsonArray>(); JsonArray masked_entitiesJson = root["masked_entities"].to<JsonArray>();
for (const EntityCustomization & entityCustomization : customizations.entityCustomizations) { for (const EntityCustomization & entityCustomization : customizations.entityCustomizations) {
JsonObject entityJson = masked_entitiesJson.add<JsonObject>(); JsonObject entityJson = masked_entitiesJson.add<JsonObject>();
entityJson["product_id"] = entityCustomization.product_id; entityJson["product_id"] = entityCustomization.product_id;
entityJson["device_id"] = entityCustomization.device_id; entityJson["device_id"] = entityCustomization.device_id;
entityJson["custom_name"] = entityCustomization.custom_name; entityJson["custom_name"] = entityCustomization.custom_name;
entityJson["custom_brand"] = entityCustomization.custom_brand;
// entries are in the form <XX><shortname>[optional customname] e.g "08heatingactive|heating is on" // entries are in the form <XX><shortname>[optional customname] e.g "08heatingactive|heating is on"
JsonArray masked_entityJson = entityJson["entity_ids"].to<JsonArray>(); JsonArray masked_entityJson = entityJson["entity_ids"].to<JsonArray>();
@@ -134,7 +135,7 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c
analog.type = analogJson["type"]; analog.type = analogJson["type"];
analog.is_system = analogJson["is_system"] | false; analog.is_system = analogJson["is_system"] | false;
if (_start && analog.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && analog.uom > DeviceValue::DeviceValueUOM::NONE) { if (_start && analog.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && analog.uom > DeviceValue::DeviceValueUOM::NONE) {
analog.offset = analog.uom - 1; analog.offset = analog.uom > 1 ? 1 : 0;
} }
customizations.analogCustomizations.push_back(analog); // add to list customizations.analogCustomizations.push_back(analog); // add to list
} }
@@ -146,10 +147,11 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c
if (root["masked_entities"].is<JsonArray>()) { if (root["masked_entities"].is<JsonArray>()) {
auto masked_entities = root["masked_entities"].as<JsonArray>(); auto masked_entities = root["masked_entities"].as<JsonArray>();
for (const JsonObject masked_entity : masked_entities) { for (const JsonObject masked_entity : masked_entities) {
auto emsEntity = EntityCustomization(); auto emsEntity = EntityCustomization();
emsEntity.product_id = masked_entity["product_id"]; emsEntity.product_id = masked_entity["product_id"];
emsEntity.device_id = masked_entity["device_id"]; emsEntity.device_id = masked_entity["device_id"];
emsEntity.custom_name = masked_entity["custom_name"] | ""; emsEntity.custom_name = masked_entity["custom_name"] | "";
emsEntity.custom_brand = masked_entity["custom_brand"] | "";
auto masked_entity_ids = masked_entity["entity_ids"].as<JsonArray>(); auto masked_entity_ids = masked_entity["entity_ids"].as<JsonArray>();
for (const JsonVariant masked_entity_id : masked_entity_ids) { for (const JsonVariant masked_entity_id : masked_entity_ids) {
@@ -242,17 +244,19 @@ void WebCustomizationService::writeDeviceName(AsyncWebServerRequest * request, J
uint8_t unique_device_id = json["id"]; uint8_t unique_device_id = json["id"];
// find product id and device id using the unique id // find product id and device id using the unique id
if (emsdevice->unique_id() == unique_device_id) { if (emsdevice->unique_id() == unique_device_id) {
uint8_t product_id = emsdevice->product_id(); uint8_t product_id = emsdevice->product_id();
uint8_t device_id = emsdevice->device_id(); uint8_t device_id = emsdevice->device_id();
auto custom_name = json["name"].as<std::string>(); std::string custom_name = json["name"] | "";
std::string custom_brand = json["brand"] | "";
// updates current record or creates a new one // updates current record or creates a new one
bool entry_exists = false; bool entry_exists = false;
update([&](WebCustomization & settings) { update([&](WebCustomization & settings) {
for (auto it = settings.entityCustomizations.begin(); it != settings.entityCustomizations.end();) { for (auto it = settings.entityCustomizations.begin(); it != settings.entityCustomizations.end();) {
if ((*it).product_id == product_id && (*it).device_id == device_id) { if ((*it).product_id == product_id && (*it).device_id == device_id) {
(*it).custom_name = custom_name; (*it).custom_name = custom_name;
entry_exists = true; (*it).custom_brand = custom_brand;
entry_exists = true;
break; break;
} else { } else {
++it; ++it;
@@ -262,9 +266,10 @@ void WebCustomizationService::writeDeviceName(AsyncWebServerRequest * request, J
// if we don't have any customization for this device, create a new entry // if we don't have any customization for this device, create a new entry
if (!entry_exists) { if (!entry_exists) {
EntityCustomization new_entry; EntityCustomization new_entry;
new_entry.product_id = product_id; new_entry.product_id = product_id;
new_entry.device_id = device_id; new_entry.device_id = device_id;
new_entry.custom_name = custom_name; new_entry.custom_name = custom_name;
new_entry.custom_brand = custom_brand;
settings.entityCustomizations.push_back(new_entry); settings.entityCustomizations.push_back(new_entry);
} }
@@ -273,6 +278,7 @@ void WebCustomizationService::writeDeviceName(AsyncWebServerRequest * request, J
// update the EMS Device record real-time // update the EMS Device record real-time
emsdevice->custom_name(custom_name); emsdevice->custom_name(custom_name);
emsdevice->custom_brand(custom_brand);
} }
} }
} }
@@ -459,10 +465,11 @@ void WebCustomizationService::load_test_data() {
// EMS entities, mark some as favorites // EMS entities, mark some as favorites
webCustomization.entityCustomizations.clear(); webCustomization.entityCustomizations.clear();
auto emsEntity = EntityCustomization(); auto emsEntity = EntityCustomization();
emsEntity.product_id = 123; emsEntity.product_id = 123;
emsEntity.device_id = 8; emsEntity.device_id = 8;
emsEntity.custom_name = "My Custom Boiler"; emsEntity.custom_name = "My Custom Boiler";
emsEntity.custom_brand = "";
emsEntity.entity_ids.push_back("08heatingactive|is my heating on?"); emsEntity.entity_ids.push_back("08heatingactive|is my heating on?");
emsEntity.entity_ids.push_back("08tapwateractive"); emsEntity.entity_ids.push_back("08tapwateractive");
emsEntity.entity_ids.push_back("08selflowtemp|<90"); emsEntity.entity_ids.push_back("08selflowtemp|<90");
@@ -472,6 +479,7 @@ void WebCustomizationService::load_test_data() {
for (const auto & emsdevice : EMSESP::emsdevices) { for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice->is_device_id(emsEntity.device_id)) { if (emsdevice->is_device_id(emsEntity.device_id)) {
emsdevice->custom_name(emsEntity.custom_name); emsdevice->custom_name(emsEntity.custom_name);
emsdevice->custom_brand(emsEntity.custom_brand);
break; break;
} }
} }

View File

@@ -63,10 +63,11 @@ class AnalogCustomization {
// we use product_id and device_id to make the device unique // we use product_id and device_id to make the device unique
class EntityCustomization { class EntityCustomization {
public: public:
uint8_t product_id; // device's product id uint8_t product_id; // device's product id
uint8_t device_id; // device's device id uint8_t device_id; // device's device id
std::string custom_name; // custom device name std::string custom_name; // custom device name
std::vector<std::string> entity_ids; // array of entity ids with masks and optional custom fullname std::string custom_brand; // custom brand name
std::vector<std::string> entity_ids; // array of entity ids with masks and optional custom fullname
}; };
class WebCustomization { class WebCustomization {

View File

@@ -102,7 +102,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
if (webScheduler.scheduleItems.back().name[0] != '\0') { if (webScheduler.scheduleItems.back().name[0] != '\0') {
char key[sizeof(webScheduler.scheduleItems.back().name) + 2]; char key[sizeof(webScheduler.scheduleItems.back().name) + 2];
snprintf(key, sizeof(key), "s:%s", webScheduler.scheduleItems.back().name); snprintf(key, sizeof(key), "s:%s", webScheduler.scheduleItems.back().name);
if (EMSESP::nvs_.isKey(key)) { if (EMSESP::nvs_.isKey(key) && webScheduler.scheduleItems.back().flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
webScheduler.scheduleItems.back().active = EMSESP::nvs_.getBool(key); webScheduler.scheduleItems.back().active = EMSESP::nvs_.getBool(key);
} }
Command::add( Command::add(
@@ -138,20 +138,11 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id,
publish(); publish();
} }
// save new state to nvs #2946 // save new state to nvs #2946
char key[sizeof(scheduleItem.name) + 2]; if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
snprintf(key, sizeof(key), "s:%s", scheduleItem.name); char key[sizeof(scheduleItem.name) + 2];
EMSESP::nvs_.putBool(key, scheduleItem.active); snprintf(key, sizeof(key), "s:%s", scheduleItem.name);
/* save to filesystem EMSESP::nvs_.putBool(key, scheduleItem.active);
EMSESP::webSchedulerService.update([&](WebScheduler & webSchedule) { }
for (auto si : webSchedule.scheduleItems) {
if (!strcmp(si.name, scheduleItem.name)) {
si.active = scheduleItem.active;
break;
}
}
return StateUpdateResult::CHANGED;
});
*/
return true; return true;
} }
} }
@@ -184,6 +175,15 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
return true; return true;
} }
if (!strcmp(cmd, F_(metrics))) {
std::string metrics = get_metrics_prometheus();
if (!metrics.empty()) {
output["api_data"] = metrics;
return true;
}
return false;
}
const char * attribute_s = Command::get_attribute(cmd); const char * attribute_s = Command::get_attribute(cmd);
for (const ScheduleItem & scheduleItem : *scheduleItems_) { for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (Helpers::toLower(scheduleItem.name) == cmd) { if (Helpers::toLower(scheduleItem.name) == cmd) {
@@ -195,6 +195,21 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) {
return false; // not found return false; // not found
} }
// generate Prometheus metrics format from scheduler values
std::string WebSchedulerService::get_metrics_prometheus() {
std::string result;
result.reserve(scheduleItems_->size() * 140);
for (const ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.name[0] == '\0') {
continue;
}
result += (std::string) "# HELP emsesp_" + scheduleItem.name + " " + scheduleItem.name + ", boolean, readable, visible, writable\n";
result += (std::string) "# TYPE emsesp_" + scheduleItem.name + " gauge\n";
result += (std::string) "emsesp_" + scheduleItem.name + " " + (scheduleItem.active ? "1\n" : "0\n");
}
return result;
}
// build the json for specific entity // build the json for specific entity
void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & scheduleItem) { void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & scheduleItem) {
output["name"] = (const char *)scheduleItem.name; output["name"] = (const char *)scheduleItem.name;
@@ -483,6 +498,10 @@ void WebSchedulerService::loop() {
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())); command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
scheduleItem.active = false; scheduleItem.active = false;
publish_single(scheduleItem.name, false);
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
}
} }
} }

View File

@@ -88,6 +88,8 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
uint8_t count_entities(bool cmd_only = false); uint8_t count_entities(bool cmd_only = false);
bool onChange(const char * cmd); bool onChange(const char * cmd);
std::string get_metrics_prometheus();
std::string raw_value; std::string raw_value;
std::string computed_value; std::string computed_value;

View File

@@ -31,7 +31,8 @@ void test_2() {
"off\",\"heatingtemp\":\"heating temperature\",\"maintenance\":\"maintenance scheduled\",\"maintenancedate\":\"next maintenance " "off\",\"heatingtemp\":\"heating temperature\",\"maintenance\":\"maintenance scheduled\",\"maintenancedate\":\"next maintenance "
"date\",\"maintenancetime\":\"time to next maintenance\",\"nofrostmode\":\"nofrost mode\",\"nofrosttemp\":\"nofrost " "date\",\"maintenancetime\":\"time to next maintenance\",\"nofrostmode\":\"nofrost mode\",\"nofrosttemp\":\"nofrost "
"temperature\",\"nompower\":\"nominal Power\",\"nrgheat\":\"energy heating\",\"pumpcharacter\":\"boiler pump characteristic\",\"pumpdelay\":\"pump " "temperature\",\"nompower\":\"nominal Power\",\"nrgheat\":\"energy heating\",\"pumpcharacter\":\"boiler pump characteristic\",\"pumpdelay\":\"pump "
"delay\",\"pumpmode\":\"boiler pump mode\",\"pumpmodmax\":\"boiler pump max power\",\"pumpmodmin\":\"boiler pump min power\",\"pumpontemp\":\"pump " "delay\",\"pumpkickday\":\"pump kick day\",\"pumpkickdelay\":\"pump kick delay\",\"pumpkickhour\":\"pump kick hour\",\"pumpmode\":\"boiler pump "
"mode\",\"pumpmodmax\":\"boiler pump max power\",\"pumpmodmin\":\"boiler pump min power\",\"pumpontemp\":\"pump "
"logic temperature\",\"reset\":\"reset\",\"selburnpow\":\"burner selected max power\",\"selflowtemp\":\"selected flow " "logic temperature\",\"reset\":\"reset\",\"selburnpow\":\"burner selected max power\",\"selflowtemp\":\"selected flow "
"temperature\",\"summertemp\":\"summer temperature\"}]"; "temperature\",\"summertemp\":\"summer temperature\"}]";
TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/boiler/commands")); TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/boiler/commands"));