diff --git a/.clang-format b/.clang-format index 3c2abfac4..9709ecad0 100644 --- a/.clang-format +++ b/.clang-format @@ -30,6 +30,7 @@ BreakConstructorInitializersBeforeComma: true # Always break constructor initial ExperimentalAutoDetectBinPacking: false KeepEmptyLinesAtTheStartOfBlocks: false MaxEmptyLinesToKeep: 1 +InsertNewlineAtEOF: true # clang-format 16+: one newline at end of file if missing PenaltyBreakBeforeFirstCallParameter: 200 PenaltyExcessCharacter: 10 PointerAlignment: Middle diff --git a/src/ESP32React/APSettingsService.cpp b/src/ESP32React/APSettingsService.cpp index d53017b9c..a224e8d27 100644 --- a/src/ESP32React/APSettingsService.cpp +++ b/src/ESP32React/APSettingsService.cpp @@ -64,6 +64,7 @@ StateUpdateResult APSettings::update(JsonObject root, APSettings & settings) { if (appliedOnce && modeChanged) { emsesp::EMSESP::network_.schedule_reconnect(); } + appliedOnce = true; return StateUpdateResult::CHANGED; diff --git a/src/ESP32React/ArduinoJsonJWT.cpp b/src/ESP32React/ArduinoJsonJWT.cpp index bbc2b11ac..a5eceb6c8 100644 --- a/src/ESP32React/ArduinoJsonJWT.cpp +++ b/src/ESP32React/ArduinoJsonJWT.cpp @@ -102,6 +102,7 @@ String ArduinoJsonJWT::encode(const char * cstr, int inputLen) { while (result.length() > 0 && result.charAt(result.length() - 1) == '=') { result.remove(result.length() - 1); } + result.replace('+', '-'); result.replace('/', '_'); @@ -129,4 +130,4 @@ String ArduinoJsonJWT::decode(String value) { // return as string return result; -} \ No newline at end of file +} diff --git a/src/ESP32React/ESP32React.cpp b/src/ESP32React/ESP32React.cpp index 79a3fb843..1c002a5b7 100644 --- a/src/ESP32React/ESP32React.cpp +++ b/src/ESP32React/ESP32React.cpp @@ -52,6 +52,7 @@ class StaticContentHandler : public AsyncWebHandler { return &WWW_ASSETS[i]; } } + return nullptr; } @@ -62,6 +63,7 @@ class StaticContentHandler : public AsyncWebHandler { if (cached == nullptr) { cached = lookup("/index.html"); } + return cached; } }; @@ -91,6 +93,7 @@ void ESP32React::begin() { defaultHeaders.addHeader(asyncsrv::T_CORS_ACAH, "Accept, Content-Type, Authorization"); defaultHeaders.addHeader(asyncsrv::T_CORS_ACAC, "true"); } + defaultHeaders.addHeader(asyncsrv::T_Server, networkSettings.hostname); }); _apSettingsService.begin(); diff --git a/src/ESP32React/FSPersistence.h b/src/ESP32React/FSPersistence.h index d41e6b53d..1f65d106a 100644 --- a/src/ESP32React/FSPersistence.h +++ b/src/ESP32React/FSPersistence.h @@ -35,6 +35,7 @@ class FSPersistence { settingsFile.close(); return; } + settingsFile.close(); } diff --git a/src/ESP32React/IPUtils.h b/src/ESP32React/IPUtils.h index 528c36494..4a9f9c038 100644 --- a/src/ESP32React/IPUtils.h +++ b/src/ESP32React/IPUtils.h @@ -8,6 +8,7 @@ class IPUtils { static bool isSet(const IPAddress & ip) { return ip != getNotSetIP(); } + static bool isNotSet(const IPAddress & ip) { return ip == getNotSetIP(); } @@ -19,4 +20,4 @@ class IPUtils { } }; -#endif \ No newline at end of file +#endif diff --git a/src/ESP32React/JsonUtils.h b/src/ESP32React/JsonUtils.h index 7870f34a6..f13121165 100644 --- a/src/ESP32React/JsonUtils.h +++ b/src/ESP32React/JsonUtils.h @@ -14,14 +14,17 @@ class JsonUtils { if (!defaultIp.fromString(def)) { defaultIp = INADDR_NONE; } + readIP(root, key, ip, defaultIp); } + static void readIP(JsonObject root, const String & key, IPAddress & ip, const IPAddress & defaultIp = INADDR_NONE) { const JsonVariant value = root[key]; if (!value.is() || !ip.fromString(value.as())) { ip = defaultIp; } } + static void writeIP(JsonObject root, const String & key, const IPAddress & ip) { if (IPUtils::isSet(ip)) { char ipStr[16]; diff --git a/src/ESP32React/MqttSettingsService.cpp b/src/ESP32React/MqttSettingsService.cpp index 86c179bd7..c2102aed8 100644 --- a/src/ESP32React/MqttSettingsService.cpp +++ b/src/ESP32React/MqttSettingsService.cpp @@ -38,6 +38,7 @@ void MqttSettingsService::startClient() { if ((isSecure && _state.enableTLS) || (!isSecure && !_state.enableTLS)) { return; } + delete _mqttClient; _mqttClient = nullptr; } @@ -49,10 +50,12 @@ void MqttSettingsService::startClient() { } else { _mqttClient = new espMqttClientSecure(EMSESP_MQTT_PRIORITY, EMSESP_MQTT_RUNNING_CORE); } + if (!_mqttClient) { emsesp::EMSESP::logger().warning("MQTT Client alloc failed"); return; } + static_cast(_mqttClient)->onConnect([this](bool sessionPresent) { onMqttConnect(sessionPresent); }); static_cast(_mqttClient)->onDisconnect([this](espMqttClientTypes::DisconnectReason reason) { onMqttDisconnect(reason); }); static_cast(_mqttClient) @@ -62,12 +65,14 @@ void MqttSettingsService::startClient() { }); return; } + isSecure = false; if (emsesp::EMSESP::system_.PSram() == 0) { _mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO); } else { _mqttClient = new espMqttClient(EMSESP_MQTT_PRIORITY, EMSESP_MQTT_RUNNING_CORE); } + static_cast(_mqttClient)->onConnect([this](bool sessionPresent) { onMqttConnect(sessionPresent); }); static_cast(_mqttClient)->onDisconnect([this](espMqttClientTypes::DisconnectReason reason) { onMqttDisconnect(reason); }); static_cast(_mqttClient) @@ -82,14 +87,17 @@ void MqttSettingsService::loop() { // emsesp::EMSESP::logger().info("Network connection dropped, stopping MQTT client"); _mqttClient->disconnect(true); } + if (_reconfigureMqtt || (_disconnectedAt && static_cast(uuid::get_uptime() - _disconnectedAt) >= MQTT_RECONNECTION_DELAY)) { // configure MQTT client if (_reconfigureMqtt && _state.enabled) { emsesp::EMSESP::logger().info("Starting MQTT service"); } + _disconnectedAt = configureMqtt() ? 0 : uuid::get_uptime(); _reconfigureMqtt = false; } + if (emsesp::EMSESP::system_.PSram() == 0) { _mqttClient->loop(); } @@ -188,21 +196,25 @@ bool MqttSettingsService::configureMqtt() { String certificate = "-----BEGIN CERTIFICATE-----\n" + _state.rootCA + "\n-----END CERTIFICATE-----\n"; static_cast(_mqttClient)->setCACert(certificate.c_str()); } + static_cast(_mqttClient)->setServer(_state.host.c_str(), _state.port); if (_state.username.length() > 0) { static_cast(_mqttClient) ->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr); } + static_cast(_mqttClient)->setClientId(_state.clientId.c_str()); static_cast(_mqttClient)->setKeepAlive(_state.keepAlive); static_cast(_mqttClient)->setCleanSession(_state.cleanSession); static_cast(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain return (_mqttClient->connect()); } + static_cast(_mqttClient)->setServer(_state.host.c_str(), _state.port); if (_state.username.length() > 0) { static_cast(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr); } + static_cast(_mqttClient)->setClientId(_state.clientId.c_str()); static_cast(_mqttClient)->setKeepAlive(_state.keepAlive); static_cast(_mqttClient)->setCleanSession(_state.cleanSession); @@ -322,6 +334,7 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings) if (newSettings.publish_single) { newSettings.ha_enabled = false; } + changed = true; } @@ -338,6 +351,7 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings) if (newSettings.ha_enabled) { newSettings.publish_single = false; } + changed = true; } @@ -387,9 +401,11 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings) if (newSettings.rootCA.length() == 0 && newSettings.enableTLS) { newSettings.rootCA = "insecure"; } + if (newSettings.enableTLS != settings.enableTLS || newSettings.rootCA != settings.rootCA) { changed = true; } + // save the new settings settings = newSettings; diff --git a/src/ESP32React/NTPSettingsService.cpp b/src/ESP32React/NTPSettingsService.cpp index 9353c1de7..2e992aa4a 100644 --- a/src/ESP32React/NTPSettingsService.cpp +++ b/src/ESP32React/NTPSettingsService.cpp @@ -97,8 +97,10 @@ StateUpdateResult NTPSettings::update(JsonObject root, NTPSettings & settings) { while (settings.tzFormat.length() < 40) { settings.tzFormat += " "; } + while (settings.tzFormatT.length() < 40) { settings.tzFormatT += " "; } + return StateUpdateResult::CHANGED; } diff --git a/src/ESP32React/NTPStatus.cpp b/src/ESP32React/NTPStatus.cpp index 424a51c60..c5df51b10 100644 --- a/src/ESP32React/NTPStatus.cpp +++ b/src/ESP32React/NTPStatus.cpp @@ -45,6 +45,7 @@ void NTPStatus::ntpStatus(AsyncWebServerRequest * request) { return 1; } } + return 0; }(); @@ -59,4 +60,4 @@ void NTPStatus::ntpStatus(AsyncWebServerRequest * request) { response->setLength(); request->send(response); -} \ No newline at end of file +} diff --git a/src/ESP32React/NetworkStatus.cpp b/src/ESP32React/NetworkStatus.cpp index 129ce5fa5..818c20e23 100644 --- a/src/ESP32React/NetworkStatus.cpp +++ b/src/ESP32React/NetworkStatus.cpp @@ -9,6 +9,7 @@ void add_ipv6(JsonObject root, NetworkInterface & netif) { if (found.empty()) { return; } + JsonArray addresses = root["ipv6"].to(); for (const auto & ipv6 : found) { JsonObject entry = addresses.add(); @@ -28,10 +29,12 @@ void add_gateways(JsonObject root, NetworkInterface & netif) { if (IPUtils::isSet(netif.gatewayIP())) { root["gateway_ip"] = netif.gatewayIP().toString(); } + auto found = emsesp::Network::ipv6_gateways(netif); if (found.empty()) { return; } + JsonArray gateways = root["gateway_ipv6"].to(); for (const auto & ip : found) { gateways.add(ip.toString()); diff --git a/src/ESP32React/SecurityManager.cpp b/src/ESP32React/SecurityManager.cpp index d190cecf0..f164e0f8d 100644 --- a/src/ESP32React/SecurityManager.cpp +++ b/src/ESP32React/SecurityManager.cpp @@ -36,6 +36,7 @@ void SecurityManager::ensureRestDispatcher(AsyncWebServer * server) { if (_restDispatcherInstalled || server == nullptr) { return; } + _restDispatcherInstalled = true; server->addHandler(new RestCatchAllHandler(this)); } @@ -47,6 +48,7 @@ void SecurityManager::dispatchRest(AsyncWebServerRequest * request, JsonVariant if (!route.method.matches(method)) { continue; } + if (!route.uri.matches(request)) { continue; } @@ -64,6 +66,7 @@ void SecurityManager::dispatchRest(AsyncWebServerRequest * request, JsonVariant } else { route.plainHandler(request); } + return; } @@ -78,11 +81,13 @@ bool SecurityManager::RestCatchAllHandler::canHandle(AsyncWebServerRequest * req if (!request->isHTTP()) { return false; } + for (const auto & route : _owner->_restRoutes) { if (route.method.matches(request->method()) && route.uri.matches(request)) { return true; } } + return false; } @@ -96,6 +101,7 @@ void SecurityManager::RestCatchAllHandler::handleBody(AsyncWebServerRequest * re if (total == 0 || total > kMaxBodySize || !isJsonContent(request)) { return; } + if (index == 0 && request->_tempObject == nullptr) { request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // freed by request destructor if (request->_tempObject == nullptr) { @@ -103,6 +109,7 @@ void SecurityManager::RestCatchAllHandler::handleBody(AsyncWebServerRequest * re return; } } + if (request->_tempObject != nullptr) { memcpy(static_cast(request->_tempObject) + index, data, len); } @@ -116,8 +123,10 @@ void SecurityManager::RestCatchAllHandler::handleRequest(AsyncWebServerRequest * request->send(400); return; } + json = doc.as(); } + _owner->dispatchRest(request, json); } diff --git a/src/ESP32React/SecurityManager.h b/src/ESP32React/SecurityManager.h index 7c8efc831..442be5dcd 100644 --- a/src/ESP32React/SecurityManager.h +++ b/src/ESP32React/SecurityManager.h @@ -50,9 +50,11 @@ class AuthenticationPredicates { (void)authentication; return true; }; + static bool IS_AUTHENTICATED(const Authentication & authentication) { return authentication.authenticated; }; + static bool IS_ADMIN(const Authentication & authentication) { return authentication.authenticated && authentication.user->admin; }; diff --git a/src/ESP32React/SecuritySettingsService.cpp b/src/ESP32React/SecuritySettingsService.cpp index 6250b7ad2..1eaf22e2f 100644 --- a/src/ESP32React/SecuritySettingsService.cpp +++ b/src/ESP32React/SecuritySettingsService.cpp @@ -29,6 +29,7 @@ Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerReques String value = tokenParamater->value(); return authenticateJWT(value); } + return {}; } @@ -48,6 +49,7 @@ Authentication SecuritySettingsService::authenticateJWT(String & jwt) { } } } + return {}; } @@ -57,6 +59,7 @@ Authentication SecuritySettingsService::authenticate(const String & username, co return Authentication(_user); } } + return {}; } @@ -91,5 +94,6 @@ void SecuritySettingsService::generateToken(AsyncWebServerRequest * request) { return; } } + request->send(401); } diff --git a/src/ESP32React/SecuritySettingsService.h b/src/ESP32React/SecuritySettingsService.h index 8455a93c9..cc2d37e42 100644 --- a/src/ESP32React/SecuritySettingsService.h +++ b/src/ESP32React/SecuritySettingsService.h @@ -62,6 +62,7 @@ class SecuritySettings { settings.users.emplace_back(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true); settings.users.emplace_back(FACTORY_GUEST_USERNAME, FACTORY_GUEST_PASSWORD, false); } + return StateUpdateResult::CHANGED; } }; @@ -90,4 +91,4 @@ class SecuritySettingsService final : public StatefulService, boolean validatePayload(JsonObject parsedPayload, const User * user); // Verify the payload is correct }; -#endif \ No newline at end of file +#endif diff --git a/src/ESP32React/StatefulService.h b/src/ESP32React/StatefulService.h index ec9445bb6..9b35fee73 100644 --- a/src/ESP32React/StatefulService.h +++ b/src/ESP32React/StatefulService.h @@ -50,6 +50,7 @@ class StatefulService { if (!cb) { return 0; } + StateUpdateHandlerInfo_t updateHandler(std::move(cb), allowRemove); _updateHandlers.push_back(std::move(updateHandler)); return updateHandler._id; @@ -73,6 +74,7 @@ class StatefulService { if (result == StateUpdateResult::CHANGED) { callUpdateHandlers(); } + return result; } @@ -90,6 +92,7 @@ class StatefulService { if (result == StateUpdateResult::CHANGED) { callUpdateHandlers(); } + return result; } diff --git a/src/ESP32React/UploadFileService.cpp b/src/ESP32React/UploadFileService.cpp index 67d950fa5..bb4d1acb9 100644 --- a/src/ESP32React/UploadFileService.cpp +++ b/src/ESP32React/UploadFileService.cpp @@ -55,6 +55,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri } else { emsesp::EMSESP::logger().info("MD5 digest received (%s). Now upload the firmware BIN", _ota.md5()); } + return; default: _ota.clearMd5(); @@ -78,6 +79,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri handleError(request, 406); return; } + _is_recovery = true; emsesp::EMSESP::logger().info("Recovery firmware uploading to the %s partition (size: %dKB). Please wait...", target->label, filesize / 1024); } else @@ -96,6 +98,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri if (_ota.md5Applied()) { emsesp::EMSESP::logger().info("Firmware MD5 check enabled (%s)", _ota.md5()); } + request->onDisconnect([this] { handleDisconnect(); }); // success, let's make sure we end the update if the client hangs up } else { emsesp::EMSESP::logger().err("OTA begin failed: %s", _ota.errorString()); @@ -133,6 +136,7 @@ void UploadFileService::handleUpload(AsyncWebServerRequest * request, const Stri handleError(request, 500); // internal error, failed return; } + if (final) { if (!_ota.end()) { emsesp::EMSESP::logger().err("OTA update failed: %s", _ota.errorString()); @@ -156,6 +160,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) { request->send(request->beginResponse(code)); _response_sent = true; } + _is_firmware = false; _is_filesystem = false; _is_recovery = false; @@ -187,6 +192,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) { emsesp::EMSESP::logger().info("Firmware MD5 matches"); root["md5_ok"] = true; } + response->setLength(); request->send(response); _ota.clearMd5(); @@ -214,6 +220,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) { AsyncWebServerResponse * response = request->beginResponse(200); request->send(response); } + emsesp::EMSESP::system_.systemStatus( emsesp::SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART); // will be handled by the main loop. We use pending for the Web's SystemMonitor return; @@ -237,6 +244,7 @@ void UploadFileService::handleError(AsyncWebServerRequest * request, int code) { if (request->_tempObject) { return; } + request->_tempObject = reinterpret_cast(static_cast(code)); // stop later chunks from retrying the write emsesp::EMSESP::logger().info("Upload error: %d", code); diff --git a/src/ESP32React/WiFiScanner.cpp b/src/ESP32React/WiFiScanner.cpp index 9cd299379..456bd02c2 100644 --- a/src/ESP32React/WiFiScanner.cpp +++ b/src/ESP32React/WiFiScanner.cpp @@ -34,9 +34,10 @@ void WiFiScanner::listNetworks(AsyncWebServerRequest * request) { network["channel"] = WiFi.channel(i); network["encryption_type"] = static_cast(WiFi.encryptionType(i)); } + response->setLength(); request->send(response); } else { request->send(202); // special code to indicate scan in progress } -} \ No newline at end of file +} diff --git a/src/core/ModuleLibrary.h b/src/core/ModuleLibrary.h index 491e15c6d..388841cd1 100644 --- a/src/core/ModuleLibrary.h +++ b/src/core/ModuleLibrary.h @@ -34,6 +34,7 @@ class ModuleLibrary { : key(key) , module(std::move(module)) { } + const char * key; std::unique_ptr module; }; diff --git a/src/core/analogsensor.cpp b/src/core/analogsensor.cpp index ec1d323e9..a77605203 100644 --- a/src/core/analogsensor.cpp +++ b/src/core/analogsensor.cpp @@ -35,22 +35,27 @@ void IRAM_ATTR AnalogSensor::freqIrq0() { edgecnt[0] = edgecnt[0] + 1; edge[0] = micros(); } + portEXIT_CRITICAL_ISR(&mux); } + void IRAM_ATTR AnalogSensor::freqIrq1() { portENTER_CRITICAL_ISR(&mux); if (micros() - edge[1] > 10) { // limit to 100kHz edgecnt[1] = edgecnt[1] + 1; edge[1] = micros(); } + portEXIT_CRITICAL_ISR(&mux); } + void IRAM_ATTR AnalogSensor::freqIrq2() { portENTER_CRITICAL_ISR(&mux); if (micros() - edge[2] > 10) { // limit to 100kHz edgecnt[2] = edgecnt[2] + 1; edge[2] = micros(); } + portEXIT_CRITICAL_ISR(&mux); } #endif @@ -89,6 +94,7 @@ void AnalogSensor::start(const bool factory_settings) { }); LOG_DEBUG("Adding system sensors for core_voltage, supply_voltage and led"); } + reload(true); // fetch the list of sensors from our customization service if (!analog_enabled_) { @@ -140,6 +146,7 @@ void AnalogSensor::reload(bool get_nvs) { sensors_.clear(); return; } + changed_ = true; // load the list of analog sensors from the customization service // and store them locally and then activate them @@ -155,11 +162,13 @@ void AnalogSensor::reload(bool get_nvs) { && (sensor_.type() != sensor.type || sensor_.offset() != sensor.offset || sensor_.factor() != sensor.factor)) { sensor_.set_value(sensor.offset); } + if ((sensor.type == AnalogType::COUNTER || (sensor.type >= AnalogType::CNT_0 && sensor.type <= AnalogType::CNT_2)) && sensor_.offset() != sensor.offset && sensor.offset != EMSESP::nvs_.getDouble(sensor.name, 0)) { EMSESP::nvs_.putDouble(sensor.name, sensor.offset); sensor_.set_value(sensor.offset); } + sensor_.set_name(sensor.name); sensor_.set_type(sensor.type); sensor_.set_offset(sensor.offset); @@ -169,6 +178,7 @@ void AnalogSensor::reload(bool get_nvs) { found = true; } } + if (!found) { it = sensors_.erase(it); } else { @@ -184,6 +194,7 @@ void AnalogSensor::reload(bool get_nvs) { found = true; } } + if (!found) { // it's new, we assume it's valid sensors_.emplace_back(sensor.gpio, sensor.name, sensor.offset, sensor.factor, sensor.uom, sensor.type, sensor.is_system); @@ -212,6 +223,7 @@ void AnalogSensor::reload(bool get_nvs) { CommandFlag::ADMIN_ONLY); } } + return true; }); @@ -225,10 +237,12 @@ void AnalogSensor::reload(bool get_nvs) { exclude_types_.push_back(sensor.type()); exclude_types_.push_back(sensor.type() + 3); } + if ((sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) { exclude_types_.push_back(sensor.type()); exclude_types_.push_back(sensor.type() - 3); } + if ((sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2)) { exclude_types_.push_back(sensor.type()); } @@ -247,6 +261,7 @@ void AnalogSensor::reload(bool get_nvs) { #endif pinMode(sensor.gpio(), INPUT_PULLUP); } + if (sensor.type() == AnalogType::ADC) { LOG_DEBUG("ADC Sensor on GPIO %02d", sensor.gpio()); // analogSetPinAttenuation does not work with analogReadMilliVolts @@ -263,6 +278,7 @@ void AnalogSensor::reload(bool get_nvs) { if (double_t val = EMSESP::nvs_.getDouble(sensor.name(), 0)) { sensor.set_value(val); } + publish_sensor(sensor); } else if (sensor.type() == AnalogType::TIMER || sensor.type() == AnalogType::RATE) { LOG_DEBUG("Timer/Rate on GPIO %02d", sensor.gpio()); @@ -290,6 +306,7 @@ void AnalogSensor::reload(bool get_nvs) { if (double_t val = EMSESP::nvs_.getDouble(sensor.name(), 0)) { sensor.set_value(val); } + publish_sensor(sensor); attachInterrupt(sensor.gpio(), index == 0 ? freqIrq0 : index == 1 ? freqIrq1 : freqIrq2, FALLING); edgecnt[index] = 0; @@ -320,6 +337,7 @@ void AnalogSensor::reload(bool get_nvs) { } else if (sensor.offset() < 0) { sensor.set_offset(0); } + dacWrite(sensor.gpio(), sensor.offset()); sensor.set_value(sensor.offset()); } else @@ -330,6 +348,7 @@ void AnalogSensor::reload(bool get_nvs) { } else if (sensor.offset() < 0) { sensor.set_offset(0); } + dacWrite(sensor.gpio(), sensor.offset()); sensor.set_value(sensor.offset()); } else @@ -344,10 +363,12 @@ void AnalogSensor::reload(bool get_nvs) { } else if (sensor.uom() > 1) { sensor.set_uom(2); } + sensor.set_offset(sensor.offset() > 0 ? 1 : 0); digitalWrite(sensor.gpio(), (sensor.offset() == 0) ^ (sensor.factor() > 0)); sensor.set_value(sensor.offset()); } + publish_sensor(sensor); } else if (sensor.type() == AnalogType::PULSE) { LOG_DEBUG("Pulse on GPIO %02d", sensor.gpio()); @@ -362,6 +383,7 @@ void AnalogSensor::reload(bool get_nvs) { } else if (sensor.offset() < 0) { sensor.set_offset(0); } + ledcWrite(sensor.gpio(), (uint32_t)(sensor.offset() * 8191 / 100)); sensor.set_value(sensor.offset()); sensor.set_uom(DeviceValueUOM::PERCENT); @@ -406,6 +428,7 @@ void AnalogSensor::measure() { sensor.sum_ = (sensor.sum_ * 15 + a * 16) / 16; sensor.analog_ = sensor.sum_ / 16; } + if (sensor.analog_ > 0 && sensor.analog_ < NTC_VREF_MV && (sensor.last_reading_ + 1 < sensor.analog_ || sensor.last_reading_ > sensor.analog_ + 1)) { sensor.set_value(sensor.offset() + 1 / (1 / T25 + log((double)sensor.analog_ / (NTC_VREF_MV - sensor.analog_) * (Rt / R0)) / Beta) @@ -429,6 +452,7 @@ void AnalogSensor::measure() { } else if (micros() - edge[index] > FREQ_ZERO_TIMEOUT_US && sensor.value() > 0) { sensor.set_value(0); } + if (sensor.value() != oldval) { changed_ = true; publish_sensor(sensor); @@ -461,6 +485,7 @@ void AnalogSensor::measure() { sensor.polltime_ = uuid::get_uptime(); // remember time of pinchange sensor.poll_ = current_reading; } + // debounce and check for real pinchange if (uuid::get_uptime() - sensor.polltime_ >= 15 && sensor.poll_ != sensor.last_reading_) { sensor.last_reading_ = sensor.poll_; @@ -475,15 +500,18 @@ void AnalogSensor::measure() { } else if (sensor.type() == AnalogType::TIMER) { // default seconds with factor 1 sensor.set_value(sensor.factor() * (sensor.polltime_ - sensor.last_polltime_) / 1000); } + sensor.last_polltime_ = sensor.polltime_; } } } + if (sensor.type() == AnalogType::PULSE && sensor.value() && sensor.polltime_ && sensor.polltime_ < uuid::get_uptime()) { sensor.set_value(0); digitalWrite(sensor.gpio(), sensor.offset()); sensor.polltime_ = 0; } + // see if there is a change and increment # reads if (old_value != sensor.value()) { sensorreads_++; @@ -531,6 +559,7 @@ bool AnalogSensor::update(uint8_t gpio, const char * org_name, double offset, do } else { strlcpy(name, org_name, sizeof(name)); } + // first see if we can find the sensor in our customization list bool found_sensor = false; EMSESP::webCustomizationService.update([&](WebCustomization & settings) { @@ -545,12 +574,14 @@ bool AnalogSensor::update(uint8_t gpio, const char * org_name, double offset, do if (EMSESP::nvs_.isKey(AnalogCustomization.name)) { EMSESP::nvs_.remove(AnalogCustomization.name); } + settings.analogCustomizations.remove(AnalogCustomization); } else { // update existing record if (!strcmp(name, AnalogCustomization.name) && EMSESP::nvs_.isKey(AnalogCustomization.name)) { EMSESP::nvs_.remove(AnalogCustomization.name); } + strlcpy(AnalogCustomization.name, name, sizeof(AnalogCustomization.name)); AnalogCustomization.offset = offset; AnalogCustomization.factor = factor; @@ -559,9 +590,11 @@ bool AnalogSensor::update(uint8_t gpio, const char * org_name, double offset, do AnalogCustomization.is_system = is_system; LOG_DEBUG("Customizing existing analog GPIO %02d", gpio); } + return StateUpdateResult::CHANGED; // persist the change } } + return StateUpdateResult::UNCHANGED; }); @@ -608,6 +641,7 @@ bool AnalogSensor::updated_values() { changed_ = false; return true; } + return false; } @@ -620,14 +654,17 @@ void AnalogSensor::publish_sensor(const Sensor & sensor) const { } else { snprintf(topic, sizeof(topic), "%s%s/%s", F_(analogsensor), "_data", sensor.name()); } + char result[12]; if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::DIGITAL_OUT) { Helpers::render_boolean(result, sensor.value() != 0); } else { Helpers::render_value(result, sensor.value(), 2); // double } + Mqtt::queue_publish(topic, result); // always publish as doubles } + char cmd[COMMAND_MAX_LENGTH]; snprintf(cmd, sizeof(cmd), "%s/%s", F_(analogsensor), sensor.name()); EMSESP::webSchedulerService.onChange(cmd); @@ -661,6 +698,7 @@ void AnalogSensor::remove_ha_topic(const int8_t type, const uint8_t gpio) const } else { snprintf(topic, sizeof(topic), "sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), gpio); } + Mqtt::queue_remove_topic(topic); } @@ -677,6 +715,7 @@ void AnalogSensor::publish_values(const bool force) { for (const auto & sensor : sensors_) { publish_sensor(sensor); } + return; } else if (!EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { return; // wait for first time period @@ -728,10 +767,12 @@ void AnalogSensor::publish_values(const bool force) { snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name()); snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj); } + char sample_val[12] = "0"; if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::DIGITAL_OUT || sensor.type() == AnalogType::PULSE) { Helpers::render_boolean(sample_val, false); } + // don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery if (Mqtt::discovery_type() == Mqtt::discoveryType::HOMEASSISTANT) { config["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else 'None'}}"; @@ -840,6 +881,7 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int for (const auto & sensor : sensors_) { output[sensor.name()] = sensor.value(); } + return true; } @@ -848,6 +890,7 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int for (const auto & sensor : sensors_) { get_value_json(output[sensor.name()].to(), sensor); } + return true; } @@ -857,6 +900,7 @@ bool AnalogSensor::get_value_info(JsonObject output, const char * cmd, const int output["api_data"] = metrics; return true; } + return false; } @@ -886,12 +930,14 @@ std::string AnalogSensor::get_metrics_prometheus() { } 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) { @@ -900,6 +946,7 @@ std::string AnalogSensor::get_metrics_prometheus() { result += (std::string)(sensor.value() == 0 ? "0\n" : "1\n"); } } + return result; } @@ -970,6 +1017,7 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { if (!Helpers::value2bool(value, b)) { return false; } + val = b ? 1 : 0; } @@ -984,6 +1032,7 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { // sensor.set_offset(val); sensor.set_value(val); } + sensor.set_offset(sensor.value()); if (sensor.value() != EMSESP::nvs_.getDouble(sensor.name(), 0)) { EMSESP::nvs_.putDouble(sensor.name(), sensor.value()); @@ -1037,16 +1086,19 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { } else if (val < 0) { val = 0; } + sensor.set_offset(val); sensor.set_value(val); ledcWrite(sensor.gpio(), (uint32_t)(sensor.offset() * 8191 / 100)); } else { return false; } + if (oldoffset != sensor.offset()) { publish_sensor(sensor); changed_ = true; } + return true; } } @@ -1054,4 +1106,4 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { return false; } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/core/analogsensor.h b/src/core/analogsensor.h index a6f334a9a..f20021e1e 100644 --- a/src/core/analogsensor.h +++ b/src/core/analogsensor.h @@ -63,6 +63,7 @@ class AnalogSensor { bool is_system() const { return is_system_; } + void set_is_system(const bool is_system) { is_system_ = is_system; } @@ -70,6 +71,7 @@ class AnalogSensor { double factor() const { return factor_; } + void set_factor(const double factor) { factor_ = factor; } @@ -81,6 +83,7 @@ class AnalogSensor { void set_uom(const uint8_t uom) { uom_ = uom; } + uint8_t uom() const { return uom_; } @@ -172,6 +175,7 @@ class AnalogSensor { if (exclude_disabled_system) { return std::count_if(sensors_.begin(), sensors_.end(), [](const Sensor & sensor) { return !sensor.is_system(); }); } + return sensors_.size(); } @@ -221,4 +225,4 @@ class AnalogSensor { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/core/command.cpp b/src/core/command.cpp index ff142b726..e45fd0bca 100644 --- a/src/core/command.cpp +++ b/src/core/command.cpp @@ -35,6 +35,7 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec if (!strncmp(path, Mqtt::base().c_str(), Mqtt::base().length())) { path += Mqtt::base().length(); } + SUrlParser p; // parse URL for the path names p.parse(path); @@ -102,6 +103,7 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec if (device_type >= EMSdevice::DeviceType::BOILER) { command_p = parse_command_string(command_p, id_n); } + if (command_p == nullptr) { // handle dead endpoints like api/system or api/boiler // default to 'value' for all devices @@ -159,6 +161,7 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec if (device_type1 >= EMSdevice::DeviceType::BOILER) { data_p = parse_command_string(data_p, id_d); } + if (data_p == nullptr) { return CommandRet::INVALID; } @@ -202,6 +205,7 @@ uint8_t Command::process(const char * path, const bool is_admin, const JsonObjec } else { return json_message(CommandRet::ERROR, "cannot parse command", output); // can't process } + return return_code; } @@ -222,6 +226,7 @@ const char * Command::return_code_string(const uint8_t return_code) { default: break; } + static char s[4]; // static to avoid allocation on each call and loosing scope return Helpers::smallitoa(s, return_code); } @@ -249,6 +254,7 @@ const char * Command::parse_command_string(const char * command, int8_t & id) { for (size_t i = 0; i < len; i++) { lowerCmd[i] = tolower(command[i]); } + lowerCmd[len] = '\0'; // check prefix and valid number range, also check 'id' @@ -294,6 +300,7 @@ const char * Command::parse_command_string(const char * command, int8_t & id) { id = id_org; return cmd_org; } + command++; // return null for empty command @@ -315,6 +322,7 @@ const char * Command::get_attribute(const char * cmd) { *breakp = '\0'; return breakp + 1; } + return nullptr; } @@ -365,6 +373,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha if (command == nullptr) { return CommandRet::NOT_FOUND; } + char cmd[COMMAND_MAX_LENGTH]; strlcpy(cmd, Helpers::toLower(command).c_str(), sizeof(cmd)); @@ -378,6 +387,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha if (!strcmp(cmd, F_(commands))) { return Command::list(device_type, output); } + // for the Commands device, calling a named command executes it (using its stored value) // rather than returning its definition, so skip the value-info lookup and fall through // to the registered command function. The list keywords above are still handled. @@ -430,6 +440,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha output["message"] = err; LOG_WARNING("Command failed: %s", err.c_str()); } + return CommandRet::NOT_FOUND; } @@ -461,6 +472,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha } else { snprintf(error, sizeof(error), "Command %s: %s failed (%s)", cmd, value, return_code_string(return_code)); } + output.clear(); output["message"] = error; LOG_WARNING(error); @@ -475,6 +487,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha } else { snprintf(info_s, sizeof(info_s), "%s/%s", dname, cmd); } + if (single_command) { // log as DEBUG (TRACE) regardless if compiled with EMSESP_DEBUG logger_.debug("%sCalled command %s", ro.c_str(), info_s); @@ -552,6 +565,7 @@ void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) { return; } + const std::string cmd_lower = Helpers::toLower(cmd); auto it = cmdfunctions_.begin(); for (auto const & cf : cmdfunctions_) { @@ -559,6 +573,7 @@ void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t cmdfunctions_.erase(it); return; } + it++; } } @@ -607,6 +622,7 @@ bool Command::list(const uint8_t device_type, JsonObject output) { sorted_cmds.push_back(tagged_cmd(cf.cmd_, cf.flags_)); } } + sorted_cmds.sort(); for (const auto & cl : sorted_cmds) { @@ -616,6 +632,7 @@ bool Command::list(const uint8_t device_type, JsonObject output) { } } } + return true; } @@ -628,6 +645,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo sorted_cmds.push_back(tagged_cmd(cf.cmd_, cf.flags_)); } } + if (device_type == EMSdevice::DeviceType::SYSTEM) { sorted_cmds.emplace_back("settings/showertimer"); sorted_cmds.emplace_back("settings/showeralert"); @@ -638,6 +656,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo sorted_cmds.emplace_back("ap/enabled"); sorted_cmds.emplace_back("syslog/enabled"); } + sorted_cmds.sort(); // sort them // if not in verbose mode, just print them on a single line and exit @@ -650,6 +669,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo shell.print(cl); shell.print(" "); } + shell.println(); return; } @@ -668,6 +688,7 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo while (i++ < 30) { shell.print(' '); } + shell.print(COLOR_BRIGHT_CYAN); if (cf.has_flags(CommandFlag::CMD_FLAG_HC)) { shell.print(Helpers::translated_word(FL_(tag_hcx))); @@ -685,12 +706,14 @@ void Command::show(uuid::console::Shell & shell, uint8_t device_type, bool verbo shell.print(Helpers::translated_word(FL_(tag_srcx))); shell.print(' '); } + shell.print(Helpers::translated_word(cf.description_)); if (!cf.has_flags(CommandFlag::ADMIN_ONLY)) { shell.print(' '); shell.print(COLOR_BRIGHT_GREEN); shell.print('*'); } + shell.print(COLOR_RESET); shell.println(); } @@ -747,6 +770,7 @@ void Command::show_devices(uuid::console::Shell & shell) { if (EMSESP::sensor_enabled()) { shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::TEMPERATURESENSOR)); } + if (EMSESP::analog_enabled()) { shell.printf("%s ", EMSdevice::device_type_2_device_name(EMSdevice::DeviceType::ANALOGSENSOR)); } @@ -759,6 +783,7 @@ void Command::show_devices(uuid::console::Shell & shell) { } } } + shell.println(); } @@ -788,6 +813,7 @@ void Command::show_all(uuid::console::Shell & shell) { if (EMSESP::sensor_enabled()) { show(shell, EMSdevice::DeviceType::TEMPERATURESENSOR, true); } + if (EMSESP::analog_enabled()) { show(shell, EMSdevice::DeviceType::ANALOGSENSOR, true); } @@ -811,6 +837,7 @@ uint8_t Command::json_message(uint8_t error_code, const char * message, const Js } else { output["message"] = message; } + return error_code; } @@ -826,6 +853,7 @@ std::string SUrlParser::path() { s += f; s += "/"; } + s.pop_back(); // deleting last letter, that is slash '/' return std::string(s); } @@ -858,12 +886,14 @@ bool SUrlParser::parse(const char * uri) { m_folders.push_back(s); s.clear(); } + t = Type::folder; } else if (*c == '?' && (t == Type::folder || t == Type::begin)) { if (s.length() > 0) { m_folders.push_back(s); s.clear(); } + t = Type::param; } else if (*c == '=' && (t == Type::param || t == Type::begin)) { m_keysvalues[s] = ""; @@ -877,6 +907,7 @@ bool SUrlParser::parse(const char * uri) { m_keysvalues[s] = ""; last_param = s; } + t = Type::param; s.clear(); } else if (*c == '\0' && s.length() > 0) { @@ -888,11 +919,13 @@ bool SUrlParser::parse(const char * uri) { m_keysvalues[s] = ""; last_param = s; } + s.clear(); } else if (*c == '\0' && s.length() == 0) { if (t == Type::param && last_param.length() > 0) { m_keysvalues[last_param] = ""; } + s.clear(); } else { s += *c; diff --git a/src/core/command.h b/src/core/command.h index 1a2d2b820..4bc4afe6c 100644 --- a/src/core/command.h +++ b/src/core/command.h @@ -86,12 +86,15 @@ class Command { inline void add_flags(uint8_t flags) { flags_ |= flags; } + inline bool has_flags(uint8_t flags) const { return (flags_ & flags) == flags; } + inline void remove_flags(uint8_t flags) { flags_ &= ~flags; } + inline uint8_t flags() const { return flags_; } @@ -183,4 +186,4 @@ class SUrlParser { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/core/console.cpp b/src/core/console.cpp index be2f828b5..8b2cde31a 100644 --- a/src/core/console.cpp +++ b/src/core/console.cpp @@ -55,6 +55,7 @@ static void console_log_level(Shell & shell, const std::vector & ar return; } } + shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level())); } @@ -224,6 +225,7 @@ static void setup_commands(std::shared_ptr const & commands) { return StateUpdateResult::CHANGED; } } + // otherwise add a new admin user securitySettings.users.emplace_back(F_(admin), password2.c_str(), true); return StateUpdateResult::CHANGED; @@ -395,6 +397,7 @@ static void setup_commands(std::shared_ptr const & commands) { if (!data.empty()) { data += " "; } + data += arg; } @@ -509,6 +512,7 @@ static void setup_commands(std::shared_ptr const & commands) { if (device_type >= EMSdevice::DeviceType::BOILER) { cmd = Command::parse_command_string(cmd, id); // extract hc or dhw } + if (cmd == nullptr) { cmd = F_(values); } @@ -544,6 +548,7 @@ static void setup_commands(std::shared_ptr const & commands) { shell.println(data.c_str()); return; } + serializeJsonPretty(doc, shell); shell.println(); return; @@ -575,6 +580,7 @@ static void setup_commands(std::shared_ptr const & commands) { devices_list.emplace_back(EMSdevice::device_type_2_device_name(device_class.first)); } } + return devices_list; } else if (current_arguments.size() == 1) { uint8_t device_type = EMSdevice::device_name_2_device_type(current_arguments[0].c_str()); @@ -585,6 +591,7 @@ static void setup_commands(std::shared_ptr const & commands) { command_list.emplace_back(cf.cmd_); } } + return command_list; } } @@ -612,6 +619,7 @@ void EMSESPShell::stopped() { if (has_flags(CommandFlags::ADMIN)) { logger().log(LogLevel::INFO, LogFacility::AUTH, F("Admin session closed on console %s"), console_name().c_str()); } + logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session closed on console %s"), console_name().c_str()); } diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index f0c35d4f1..0a9e700ab 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -31,6 +31,7 @@ uint8_t EMSdevice::count_entities() { count++; } } + return count; } @@ -40,6 +41,7 @@ uint8_t EMSdevice::count_entities_fav() { for (const auto & dv : devicevalues_) { count += dv.has_state(DeviceValueState::DV_FAVORITE) && dv.hasValue(); } + return count; } @@ -50,6 +52,7 @@ bool EMSdevice::has_entities() const { return true; } } + bool found = false; EMSESP::webCustomizationService.read([&](WebCustomization & settings) { for (const auto & entityCustomization : settings.entityCustomizations) { @@ -84,6 +87,7 @@ uint8_t EMSdevice::tag_to_flag(const uint8_t tag) { } else if (tag >= DeviceValueTAG::TAG_SRC1 && tag <= DeviceValueTAG::TAG_SRC16) { return CommandFlag::CMD_FLAG_SRC; } + return 0; } @@ -112,6 +116,7 @@ const char * EMSdevice::brand_to_cstr() const { if (!custom_brand().empty()) { return custom_brand().c_str(); } + switch (brand_) { case EMSdevice::Brand::BOSCH: return F_(bosch); @@ -192,6 +197,7 @@ const char * EMSdevice::device_type_2_device_name_translated() { if (flags_ == EMS_DEVICE_FLAG_HEATPUMP) { return Helpers::translated_word(FL_(boiler_hp_device)); } + return Helpers::translated_word(FL_(boiler_device)); case DeviceType::THERMOSTAT: return Helpers::translated_word(FL_(thermostat_device)); @@ -224,6 +230,7 @@ const char * EMSdevice::device_type_2_device_name_translated() { default: break; } + return Helpers::translated_word(FL_(unknown_device)); } @@ -243,45 +250,59 @@ uint8_t EMSdevice::device_name_2_device_type(const char * topic) { if (!strcmp(lowtopic, F_(boiler))) { return DeviceType::BOILER; } + if (!strcmp(lowtopic, F_(thermostat))) { return DeviceType::THERMOSTAT; } + if (!strcmp(lowtopic, F_(scheduler))) { return DeviceType::SCHEDULER; } + if (!strcmp(lowtopic, F_(heatpump))) { return DeviceType::HEATPUMP; } + if (!strcmp(lowtopic, F_(solar))) { return DeviceType::SOLAR; } + if (!strcmp(lowtopic, F_(mixer))) { return DeviceType::MIXER; } + if (!strcmp(lowtopic, F_(switcher))) { return DeviceType::SWITCH; } + if (!strcmp(lowtopic, F_(gateway))) { return DeviceType::GATEWAY; } + if (!strcmp(lowtopic, F_(alert))) { return DeviceType::ALERT; } + if (!strcmp(lowtopic, F_(extension))) { return DeviceType::EXTENSION; } + if (!strcmp(lowtopic, F_(heatsource))) { return DeviceType::HEATSOURCE; } + if (!strcmp(lowtopic, F_(ventilation))) { return DeviceType::VENTILATION; } + if (!strcmp(lowtopic, F_(water))) { return DeviceType::WATER; } + if (!strcmp(lowtopic, F_(pool))) { return DeviceType::POOL; } + if (!strcmp(lowtopic, F_(connect))) { return DeviceType::CONNECT; } @@ -290,18 +311,23 @@ uint8_t EMSdevice::device_name_2_device_type(const char * topic) { if (!strcmp(lowtopic, F_(custom))) { return DeviceType::CUSTOM; } + if (!strcmp(lowtopic, F_(temperaturesensor))) { return DeviceType::TEMPERATURESENSOR; } + if (!strcmp(lowtopic, F_(analogsensor))) { return DeviceType::ANALOGSENSOR; } + if (!strcmp(lowtopic, F_(scheduler))) { return DeviceType::SCHEDULER; } + if (!strcmp(lowtopic, F_(commands))) { return DeviceType::COMMAND; } + if (!strcmp(lowtopic, F_(system))) { return DeviceType::SYSTEM; } @@ -403,6 +429,7 @@ bool EMSdevice::is_fetch(uint16_t telegram_id, uint8_t len) const { return tf.fetch_ && tf.length_ >= len; } } + return false; } @@ -413,6 +440,7 @@ bool EMSdevice::is_received(uint16_t telegram_id) const { return tf.received_; } } + return false; } @@ -423,6 +451,7 @@ bool EMSdevice::has_tags(const int8_t tag) const { return true; } } + return false; } @@ -433,6 +462,7 @@ bool EMSdevice::has_cmd(const char * cmd, const int8_t id) const { return true; } } + return false; } @@ -471,6 +501,7 @@ void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) const { if (telegram_functions_.empty()) { return; } + /* // colored list of type-ids shell.printf(" This %s will listen to telegram type IDs: ", device_type_name().c_str()); @@ -492,6 +523,7 @@ void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) const { shell.printf("0x%02X ", tf.telegram_type_id_); } } + shell.println(); shell.printf(" Fetched telegram type IDs: "); for (const auto & tf : telegram_functions_) { @@ -499,6 +531,7 @@ void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) const { shell.printf("0x%02X ", tf.telegram_type_id_); } } + shell.println(); shell.printf(" Pending telegram type IDs: "); for (const auto & tf : telegram_functions_) { @@ -506,11 +539,13 @@ void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) const { shell.printf("0x%02X ", tf.telegram_type_id_); } } + shell.println(); shell.printf(" Ignored telegram type IDs: "); for (auto handlers : handlers_ignored_) { shell.printf("0x%02X ", handlers); } + shell.println(); } @@ -525,6 +560,7 @@ char * EMSdevice::show_telegram_handlers(char * result, const size_t len, const if (i++ > 0) { strlcat(result, " ", len); } + strlcat(result, Helpers::hextoa(tf.telegram_type_id_, true).c_str(), len); } } @@ -535,6 +571,7 @@ char * EMSdevice::show_telegram_handlers(char * result, const size_t len, const if (i++ > 0) { strlcat(result, " ", len); } + strlcat(result, Helpers::hextoa(h).c_str(), len); } } @@ -548,6 +585,7 @@ void EMSdevice::add_handlers_ignored(const uint16_t handler) { return; } } + handlers_ignored_.push_back(handler); } @@ -623,6 +661,7 @@ void EMSdevice::add_device_value(int8_t tag, // to } else if (uom == DeviceValueUOM::PERCENT) { *(uint8_t *)(value_p) = use_max ? static_cast(max) : (test_active ? EMS_VALUE_DEFAULT_UINT8_DUMMY : EMS_VALUE_DEFAULT_UINT8); } + break; } @@ -662,6 +701,7 @@ void EMSdevice::add_device_value(int8_t tag, // to if (has_custom_name) { custom_fullname = entity_id.substr(custom_name_pos + 1); } + break; } } @@ -692,6 +732,7 @@ void EMSdevice::erase_device_values() { Command::erase_command(device_type_, dv.short_name, tag_to_flag(dv.tag)); } } + devicevalues_.clear(); telegram_functions_.clear(); handlers_ignored_.clear(); @@ -802,6 +843,7 @@ bool EMSdevice::is_readable(const void * value_p) const { return !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE); } } + return false; } @@ -814,6 +856,7 @@ bool EMSdevice::is_readonly(const std::string & cmd, const int8_t id) const { return dv.has_state(DeviceValueState::DV_READONLY); } } + return true; // not found, no write } @@ -824,6 +867,7 @@ bool EMSdevice::has_command(const void * value_p) const { return dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY); } } + return false; } @@ -879,8 +923,10 @@ void EMSdevice::publish_value(void * value_p) const { strlcpy(payload, enum_str, sizeof(payload)); } } + break; } + case DeviceValueType::UINT16: Helpers::render_value(payload, *(uint16_t *)(value_p), num_op, fahrenheit); break; @@ -905,6 +951,7 @@ void EMSdevice::publish_value(void * value_p) const { if (Helpers::hasValue((char *)(value_p))) { Helpers::render_string(payload, (char *)(value_p), sizeof(payload)); } + break; default: break; @@ -913,6 +960,7 @@ void EMSdevice::publish_value(void * value_p) const { if (Mqtt::publish_single() && payload[0] != '\0') { Mqtt::queue_publish(topic, payload); } + // check scheduler for on change char cmd[COMMAND_MAX_LENGTH]; if (dv.tag >= DeviceValueTAG::TAG_HC1) { @@ -920,6 +968,7 @@ void EMSdevice::publish_value(void * value_p) const { } else { snprintf(cmd, sizeof(cmd), "%s/%s", device_type_2_device_name(device_type_), (dv.short_name)); } + EMSESP::webSchedulerService.onChange(cmd); } } @@ -933,6 +982,7 @@ std::string EMSdevice::get_value_uom(const std::string & shortname) const { if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) { break; } + return EMSdevice::uom_to_string(dv.uom); } } @@ -949,6 +999,7 @@ bool EMSdevice::export_values(uint8_t device_type, JsonObject output, const int8 has_value |= emsdevice->generate_values(output, id, (id < 1), output_target); // use nested for id -1 and 0 } } + return has_value; } @@ -962,10 +1013,12 @@ bool EMSdevice::export_values(uint8_t device_type, JsonObject output, const int8 output_hc = output[EMSdevice::tag_to_mqtt(tag)].to(); nest_created = true; } + has_value |= emsdevice->generate_values(output_hc, tag, true, output_target); // use nested for id -1 and 0 } } } + return has_value; } @@ -1083,12 +1136,14 @@ void EMSdevice::generate_values_web(JsonObject output, const bool is_dashboard) l.add(Helpers::render_boolean(result, false, true)); l.add(Helpers::render_boolean(result, true, true)); } + // add command help template else if (dv.type == DeviceValueType::STRING || (dv.type == DeviceValueType::CMD && dv.uom == DeviceValueUOM::NONE)) { if (dv.options_size == 1) { obj["h"] = dv.options_single[0]; // NOT translated } } + // handle INTs else { // add min and max values, if available @@ -1182,6 +1237,7 @@ void EMSdevice::generate_values_web_customization(JsonArray output) { } else { obj["n"] = "!" + std::string(fullname); // prefix commands with a ! } + if (dv.has_tag()) { obj["t"] = tag_to_string(dv.tag); } @@ -1229,6 +1285,7 @@ void EMSdevice::generate_values_web_customization(JsonArray output) { obj["w"] = false; // not writeable as it won't be shown } } + break; } } @@ -1243,6 +1300,7 @@ void EMSdevice::set_climate_minmax(int8_t tag, int16_t min, uint32_t max) { dv.max = max; dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); } + return; } } @@ -1254,6 +1312,7 @@ void EMSdevice::setValueEnum(const void * value_p, const char * const ** options if (dv.options != options && Mqtt::ha_enabled()) { dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); } + dv.options = options; dv.options_size = Helpers::count_items(options); break; @@ -1324,6 +1383,7 @@ void EMSdevice::getCustomizationEntities(std::vector & entity_ids) strcpy(name, tag_to_mqtt(dv.tag)); strcat(name, "/"); } + strcat(name, dv.short_name); std::string entity_name = name; @@ -1335,6 +1395,7 @@ void EMSdevice::getCustomizationEntities(std::vector & entity_ids) break; } } + if (!is_set && (mask || dv.has_custom_fullname())) { if (!dv.has_custom_fullname()) { entity_ids.push_back(Helpers::hextoa(mask, false) + entity_name); @@ -1403,6 +1464,7 @@ void EMSdevice::dump_devicevalue_info() { Serial.print("\\|"); } } + Serial.print(']'); break; @@ -1454,6 +1516,7 @@ void EMSdevice::dump_devicevalue_info() { Serial.print(dv.max); Serial.print(")"); } + Serial.print(","); // uom @@ -1462,6 +1525,7 @@ void EMSdevice::dump_devicevalue_info() { } else { Serial.print(DeviceValue::DeviceValueUOM_s[dv.uom]); } + Serial.print(","); // writeable flag @@ -1576,9 +1640,11 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t if (!strcmp(cmd, F_(info))) { return export_values(device_type(), output, tag, EMSdevice::OUTPUT_TARGET::API_VERBOSE); } + if (!strcmp(cmd, F_(values))) { return export_values(device_type(), output, tag, EMSdevice::OUTPUT_TARGET::API_SHORTNAMES); } + if (!strcmp(cmd, F_(entities))) { char name[30]; for (auto & dv : devicevalues_) { @@ -1589,14 +1655,17 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t get_value_json(output[name].to(), dv); } } + return true; } + if (!strcmp(cmd, F_(metrics))) { std::string metrics = get_metrics_prometheus(tag); if (!metrics.empty()) { output["api_data"] = metrics; return true; } + return false; } @@ -1613,6 +1682,7 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t return Command::get_attribute(output, cmd_s, attribute_s); } } + return false; // not found, but don't return a message error yet } @@ -1649,11 +1719,13 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { json[value] = Helpers::translated_word(dv.options[*(uint8_t *)(dv.value_p)]); // text } } + json[type] = F_(enum); JsonArray enum_ = json[F_(enum)].to(); for (uint8_t i = 0; i < dv.options_size; i++) { enum_.add(Helpers::translated_word(dv.options[i])); } + break; } @@ -1661,6 +1733,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint16_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } + json[type] = F_(number); break; @@ -1668,6 +1741,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint8_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } + json[type] = F_(number); break; @@ -1675,6 +1749,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { if (Helpers::hasValue(*(int16_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(int16_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } + json[type] = F_(number); break; @@ -1682,6 +1757,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { if (Helpers::hasValue(*(int8_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(int8_t *)(dv.value_p), dv.numeric_operator, fahrenheit)); } + json[type] = F_(number); break; @@ -1690,6 +1766,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint32_t *)(dv.value_p), dv.numeric_operator)); } + json[type] = F_(number); break; @@ -1700,6 +1777,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { json["index"] = value_b ? 1 : 0; Mqtt::add_value_bool(json, value, value_b); } + json[type] = ("boolean"); break; @@ -1707,6 +1785,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { json[value] = serialized(Helpers::render_value(val, *(uint32_t *)(dv.value_p), dv.numeric_operator)); } + json[type] = F_(number); break; @@ -1715,6 +1794,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { char s[55]; json[value] = Helpers::render_string(s, (char *)(dv.value_p), sizeof(s)); } + json[type] = ("string"); break; @@ -1730,6 +1810,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { } else { json[type] = F_(command); } + break; default: @@ -1773,11 +1854,13 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { if (tag >= 0 && tag != dv.tag) { continue; } + // only count supported types if (dv.hasValue() && is_supported_type(dv.type)) { entity_count++; } } + result.reserve(160 * entity_count); for (auto & dv : devicevalues_) { @@ -1799,30 +1882,35 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { has_value = true; metric_value = (bool)*(uint8_t *)(dv.value_p) ? 1.0 : 0.0; } + break; case DeviceValueType::UINT8: if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) { has_value = true; metric_value = *(uint8_t *)(dv.value_p); } + break; case DeviceValueType::INT8: if (Helpers::hasValue(*(int8_t *)(dv.value_p))) { has_value = true; metric_value = *(int8_t *)(dv.value_p); } + break; case DeviceValueType::UINT16: if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) { has_value = true; metric_value = *(uint16_t *)(dv.value_p); } + break; case DeviceValueType::INT16: if (Helpers::hasValue(*(int16_t *)(dv.value_p))) { has_value = true; metric_value = *(int16_t *)(dv.value_p); } + break; case DeviceValueType::UINT24: case DeviceValueType::UINT32: @@ -1831,12 +1919,14 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { has_value = true; metric_value = *(uint32_t *)(dv.value_p); } + break; case DeviceValueType::ENUM: if (*(uint8_t *)(dv.value_p) < dv.options_size) { has_value = true; metric_value = *(uint8_t *)(dv.value_p); } + break; default: break; @@ -1890,8 +1980,10 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { if (i > 0) { enum_mapping += "; "; } + enum_mapping += std::to_string(i) + ": " + std::string(Helpers::translated_word(dv.options[i])); } + enum_mapping += ")"; } else if (dv.uom != DeviceValueUOM::NONE) { uom_str = uom_to_string(dv.uom); @@ -1901,6 +1993,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { if (!uom_str.empty()) { help_line += ", " + uom_str; } + if (!enum_mapping.empty()) { help_line += ", enum, " + enum_mapping; } @@ -1912,9 +2005,11 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { if (readable) { help_line += ", readable"; } + if (writeable) { help_line += ", writeable"; } + if (visible) { help_line += ", visible"; } @@ -1940,6 +2035,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { if (!circuit_label.empty()) { result += "{circuit=\"" + circuit_label + "\"}"; } + result += " "; char val_str[30]; @@ -1959,6 +2055,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } else { snprintf(val_str, sizeof(val_str), "%.2f", final_value); } + result += val_str; result += "\n"; } @@ -2092,6 +2189,7 @@ bool EMSdevice::generate_values(JsonObject output, const int8_t tag_filter, cons if (dv.numeric_operator == DeviceValueNumOp::DV_NUMOP_DIV60) { time_value /= 60; // sometimes we need to divide by 60 } + if (output_target == OUTPUT_TARGET::API_VERBOSE || output_target == OUTPUT_TARGET::CONSOLE) { char time_s[60]; snprintf(time_s, @@ -2125,6 +2223,7 @@ bool EMSdevice::generate_values(JsonObject output, const int8_t tag_filter, cons if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } + if (v < dv.min) { dv.min = v; dv.remove_state(DeviceValueState::DV_HA_CONFIG_CREATED); @@ -2174,6 +2273,7 @@ void EMSdevice::mqtt_ha_entity_config_create() { } else { dv.add_state(DeviceValueState::DV_HA_CLIMATE_NO_RT); } + dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED); count++; } @@ -2205,9 +2305,11 @@ void EMSdevice::mqtt_ha_entity_config_create() { 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); if (val != 0 && val < d.options_size) { @@ -2215,6 +2317,7 @@ void EMSdevice::mqtt_ha_entity_config_create() { } } } + Mqtt::publish_ha_climate_config(dv, true, mode_options, false, icon); count++; } @@ -2250,6 +2353,7 @@ bool EMSdevice::has_telegram_id(uint16_t id) const { return true; } } + return false; } @@ -2281,6 +2385,7 @@ bool EMSdevice::handle_telegram(const std::shared_ptr & telegram tf.process_function_(this, telegram); return true; } + // if the data block is empty and we have not received data before, assume that this telegram // is not recognized by the bus master. So remove it from the automatic fetch list if (telegram->message_length == 0 && telegram->offset == 0 && !tf.received_) { @@ -2292,8 +2397,10 @@ bool EMSdevice::handle_telegram(const std::shared_ptr & telegram if (uuid::get_uptime_sec() > FETCH_RETRY_DURATION_SEC) { tf.fetch_ = false; } + return false; } + if (telegram->message_length > 0) { tf.received_ = true; tf.process_function_(this, telegram); @@ -2302,6 +2409,7 @@ bool EMSdevice::handle_telegram(const std::shared_ptr & telegram return true; } } + return false; // type not found } @@ -2332,6 +2440,7 @@ std::string EMSdevice::name() { if (model().empty()) { return default_name(); } + return model() + "/" + std::string(default_name()); } @@ -2442,6 +2551,7 @@ int EMSdevice::modbus_value_to_json(uint8_t tag, const std::string & shortname, if (modbus_data.size() != 2) { return -2; } + jsonValue["value"] = modbus_data[0] || modbus_data[1]; } diff --git a/src/core/emsdevice.h b/src/core/emsdevice.h index 7b108c902..b10dc35e5 100644 --- a/src/core/emsdevice.h +++ b/src/core/emsdevice.h @@ -120,12 +120,15 @@ class EMSdevice { inline void add_flags(uint8_t flags) { flags_ |= flags; } + inline bool has_flags(uint8_t flags) const { return (flags_ & flags) == flags; } + inline void remove_flags(uint8_t flags) { flags_ &= ~flags; } + inline uint8_t flags() const { return flags_; } @@ -147,6 +150,7 @@ class EMSdevice { const std::string & custom_brand() const { return custom_brand_; } + // set device model void model(std::string const & model) { model_ = model; @@ -240,6 +244,7 @@ class EMSdevice { publish_value((void *)&value); return; } + ++i; } } @@ -388,6 +393,7 @@ class EMSdevice { bool ha_config_done() const { return ha_config_done_; } + void ha_config_done(const bool v) { ha_config_done_ = v; } @@ -571,6 +577,7 @@ class EMSdevice { , fetch_(fetch) { } }; + void dump_telegram_info(std::vector & telegram_functions_dump); void dump_devicevalue_info(); #endif diff --git a/src/core/emsdevicevalue.cpp b/src/core/emsdevicevalue.cpp index 68ae0eaaa..927fcd9cb 100644 --- a/src/core/emsdevicevalue.cpp +++ b/src/core/emsdevicevalue.cpp @@ -326,6 +326,7 @@ bool DeviceValue::get_min_max(int16_t & dv_set_min, uint32_t & dv_set_max) { } else { dv_set_max = Helpers::transformNumFloat(EMS_VALUE_UINT8_NOTSET - 1, numeric_operator, fahrenheit); } + return true; } @@ -337,6 +338,7 @@ bool DeviceValue::get_min_max(int16_t & dv_set_min, uint32_t & dv_set_max) { dv_set_min = Helpers::transformNumFloat(-EMS_VALUE_INT8_NOTSET + 1, numeric_operator, fahrenheit); dv_set_max = Helpers::transformNumFloat(EMS_VALUE_INT8_NOTSET - 1, numeric_operator, fahrenheit); } + return true; } @@ -359,11 +361,14 @@ bool DeviceValue::get_custom_min(int16_t & val) { if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } + if (max > 0 && v > 0 && (uint32_t)v > max) { return false; } + val = v; } + return has_min; } @@ -378,11 +383,14 @@ bool DeviceValue::get_custom_max(uint32_t & val) { if (fahrenheit) { v = (v - (32 * (fahrenheit - 1))) / 1.8; // reset to °C } + if (v < 0 || v < (int32_t)min) { return false; } + val = v; } + return has_max; } @@ -417,6 +425,7 @@ std::string DeviceValue::get_custom_fullname() const { if (minmax_pos != std::string::npos) { return cf.substr(0, minmax_pos); } + return cf; } @@ -427,6 +436,7 @@ std::string DeviceValue::get_fullname() const { if (customname.empty()) { return Helpers::translated_word(fullname); } + return customname; } @@ -436,6 +446,7 @@ std::string DeviceValue::get_name(const std::string & entity) { if (pos != std::string::npos) { return entity.substr(2, pos - 2); } + return entity.substr(2); } diff --git a/src/core/emsdevicevalue.h b/src/core/emsdevicevalue.h index 11f534dba..ba781b98f 100644 --- a/src/core/emsdevicevalue.h +++ b/src/core/emsdevicevalue.h @@ -236,12 +236,15 @@ class DeviceValue { void add_state(uint8_t s) { state |= s; } + bool has_state(uint8_t s) const { return (state & s) == s; } + void remove_state(uint8_t s) { state &= ~s; } + uint8_t get_state() const { return state; } diff --git a/src/core/emsesp.cpp b/src/core/emsesp.cpp index aafb7aa87..5e5b7db0b 100644 --- a/src/core/emsesp.cpp +++ b/src/core/emsesp.cpp @@ -120,6 +120,7 @@ void EMSESP::fetch_device_values(const uint8_t device_id) { for (const auto & emsdevice : emsdevices) { emsdevice->fetch_values(); } + return; } @@ -144,6 +145,7 @@ bool EMSESP::valid_device(const uint8_t device_id) { return true; } } + return false; // can't find it } @@ -170,6 +172,7 @@ bool EMSESP::cmd_is_readonly(const uint8_t device_type, const uint8_t device_id, return emsdevice->is_readonly(cmd, id); } } + return false; } @@ -183,6 +186,7 @@ uint8_t EMSESP::device_id_from_cmd(const uint8_t device_type, const char * cmd, return emsdevice->device_id(); } } + return 0; } @@ -202,6 +206,7 @@ void EMSESP::clear_all_devices() { for (const auto & emsdevice : emsdevices) { emsdevice->erase_device_values(); } + emsdevices.clear(); } @@ -221,6 +226,7 @@ void EMSESP::compact_entities_if_stable() { if (!entity_compaction_pending_) { return; // nothing to do (cheap early-out on the hot path) } + if ((uuid::get_uptime() - last_entity_change_) < ENTITY_COMPACT_DELAY) { return; // still settling } @@ -230,6 +236,7 @@ void EMSESP::compact_entities_if_stable() { emsdevice->compact(); } } + Command::compact(); entity_compaction_pending_ = false; @@ -248,6 +255,7 @@ uint8_t EMSESP::count_devices() { count++; } } + return count; } @@ -348,6 +356,7 @@ void EMSESP::show_ems(uuid::console::Shell & shell) { } else if ((it.telegram_->operation) == Telegram::Operation::TX_WRITE) { op = "WRITE"; } + shell.printfln(" [%02d%c] %s %s", it.id_, ((it.retry_) ? '*' : ' '), op.c_str(), pretty_telegram(it.telegram_).c_str()); } } @@ -385,10 +394,12 @@ void EMSESP::dump_all_entities(uuid::console::Shell & shell) { if (device.device_type == DeviceType::MIXER) { device_id = 0x20; // hc } + // For Heatsource set AM200 if (device.device_type == DeviceType::HEATSOURCE) { device_id = EMSdevice::EMS_DEVICE_ID_AHS1; } + // For MX400 SRC plus base if (device.device_type == DeviceType::CONNECT && device.product_id == 17) { device_id = EMSdevice::EMS_DEVICE_ID_RFBASE; @@ -510,6 +521,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) { shell.printfln(" %s: %s%s %s%s", key.c_str(), COLOR_BRIGHT_GREEN, p.value().as().c_str(), uom.c_str(), COLOR_RESET); } + shell.println(); } } @@ -524,6 +536,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) { for (JsonPair p : custom_output) { shell.printfln(" %s: %s%s%s", p.key().c_str(), COLOR_BRIGHT_GREEN, p.value().as().c_str(), COLOR_RESET); } + shell.println(); } } @@ -555,6 +568,7 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { sensor.is_system() ? "Yes" : "No"); } } + shell.println(); } @@ -588,6 +602,7 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { break; } } + shell.println(); } } @@ -650,6 +665,7 @@ void EMSESP::publish_all_loop() { if (Mqtt::ha_enabled()) { Mqtt::ha_status(); } + system_.send_heartbeat(); // send MQTT heartbeat topic break; default: @@ -698,9 +714,11 @@ void EMSESP::publish_device_values(uint8_t device_type) { json_tag = doc[EMSdevice::tag_to_mqtt(tag)].to(); nest_created = true; } + need_publish |= emsdevice->generate_values(json_tag, tag, false, EMSdevice::OUTPUT_TARGET::MQTT); } } + if (need_publish && !nested) { Mqtt::queue_publish(Mqtt::tag_to_topic(device_type, tag), json); json = doc.to(); @@ -712,6 +730,7 @@ void EMSESP::publish_device_values(uint8_t device_type) { if (doc.overflowed()) { LOG_WARNING("MQTT buffer overflow, please use individual topics"); } + Mqtt::queue_publish(Mqtt::tag_to_topic(device_type, DeviceValueTAG::TAG_NONE), json); } @@ -769,6 +788,7 @@ void EMSESP::publish_response(const std::shared_ptr & telegram) delete[] buffer; buffer = nullptr; } + if (buffer == nullptr) { offset = telegram->offset; // store offset from first part type = telegram->type_id; @@ -778,17 +798,21 @@ void EMSESP::publish_response(const std::shared_ptr & telegram) buffer[i * 3 + 1] = '0'; buffer[i * 3 + 2] = ' '; } + buffer[267] = '\0'; } + if (telegram->message_length) { strlcpy(&buffer[(telegram->offset - offset) * 3], Helpers::data_to_hex(telegram->message_data, telegram->message_length).c_str(), 768); } else { strlcpy(&buffer[(telegram->offset - offset) * 3], "", 768); } + if (response_id_ != 0) { buffer[strlen(buffer)] = ' '; // overwrite termination \0 return; // do not delete buffer } + JsonDocument doc; char s[10]; doc["src"] = Helpers::hextoa(s, telegram->src); @@ -801,6 +825,7 @@ void EMSESP::publish_response(const std::shared_ptr & telegram) uint32_t value = Helpers::hextoint(buffer); doc["value"] = value; } + Mqtt::queue_publish("response", doc.as()); delete[] buffer; buffer = nullptr; @@ -1061,6 +1086,7 @@ void EMSESP::process_UBADevices(const std::shared_ptr & telegram LOG_DEBUG("Remove EMS device with ID 0x%02X", device_id); erase_device(device_id); // } + next_byte = next_byte >> 1; // advance 1 bit } } @@ -1072,6 +1098,7 @@ void EMSESP::process_deviceName(const std::shared_ptr & telegram if (telegram->offset > 27 || (telegram->offset + telegram->message_length) < 29) { return; } + char name[16]; // len including zero terminator, if there is one, otherwise copy to end of telegram // https://github.com/emsesp/EMS-ESP32/discussions/2482#discussioncomment-12649817 @@ -1081,6 +1108,7 @@ void EMSESP::process_deviceName(const std::shared_ptr & telegram while (isprint(*c)) { c++; }; + *c = '\0'; if (strlen(name) > 2) { // https://github.com/emsesp/EMS-ESP32/issues/2166#issuecomment-2454488657 LOG_DEBUG("Model name received for device 0x%02X: %s", telegram->src, name); @@ -1177,17 +1205,21 @@ bool EMSESP::process_telegram(const std::shared_ptr & telegram) if (!trace_raw_) { LOG_TRACE("%s", pretty_telegram(telegram).c_str()); } + if (!read_next_) { response_id_ = 0; } + publish_response(telegram); } else { LOG_NOTICE("%s", pretty_telegram(telegram).c_str()); } + // check if read is finished or gives more parts if (!read_next_) { read_id_ = WATCH_ID_NONE; } + read_next_ = false; } else if (watch() == WATCH_ON) { if ((watch_id_ == WATCH_ID_NONE) || (telegram->type_id == watch_id_) @@ -1226,6 +1258,7 @@ bool EMSESP::process_telegram(const std::shared_ptr & telegram) if (!wait_km_) { process_UBADevices(telegram); } + return true; } @@ -1252,6 +1285,7 @@ bool EMSESP::process_telegram(const std::shared_ptr & telegram) found_device->has_update(false); // reset flag publish_device_values(found_device->device_type()); // publish to MQTT if we explicitly have too } + // auto publish: timeinterval 0 and publish single not set, only if queue is empty else if (mqtt_.get_publish_onchange(found_device->device_type()) && found_device->has_update() && mqtt_.publish_queued() == 0) { found_device->has_update(false); // reset flag @@ -1260,6 +1294,7 @@ bool EMSESP::process_telegram(const std::shared_ptr & telegram) } } } + break; // remove this to handle same telegrams on multiple devices } } @@ -1271,12 +1306,14 @@ bool EMSESP::process_telegram(const std::shared_ptr & telegram) if (found_device && telegram->message_length > 0) { found_device->add_handlers_ignored(telegram->type_id); } + // handle unknown broadcasted telegrams (or send to us) if (telegram->dest == 0 || telegram->dest == EMSbus::ems_bus_id()) { LOG_DEBUG("No telegram type handler found for ID 0x%02X (src 0x%02X)", telegram->type_id, telegram->src); if (watch() == WATCH_UNKNOWN) { LOG_NOTICE("%s", pretty_telegram(telegram).c_str()); } + if (!wait_km_ && !found_device && (telegram->src != EMSbus::ems_bus_id()) && (telegram->message_length > 0)) { #ifdef EMSESP_GATEWAY_TX_HOLD // a new device often sends several broadcasts before its version reply is processed, only ask once @@ -1291,6 +1328,7 @@ bool EMSESP::process_telegram(const std::shared_ptr & telegram) } } } + return telegram_found; } @@ -1300,6 +1338,7 @@ bool EMSESP::device_hasEntities(const uint8_t device_id) { return true; } } + return false; // not found } @@ -1381,12 +1420,15 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const if (product_id == 0) { // no product-id, ignore return false; } + if ((*it)->product_id() == product_id) { // update version if we have valid product_id if (!strcmp((*it)->version(), "00.00")) { (*it)->version(version); } + return true; } + // product-id has changed for this device-id, delete and re-add (*it)->erase_device_values(); emsdevices.erase(it); // erase the old device without product_id and re detect @@ -1406,6 +1448,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const device_p = &device; break; } + if (device_id >= EMSdevice::EMS_DEVICE_ID_HS1 && device_id <= EMSdevice::EMS_DEVICE_ID_HS16) { device_p = &device; device_p->device_type = DeviceType::HEATSOURCE; @@ -1442,11 +1485,13 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const default_name = "Wireless sensor base"; } } + // map MX400 also to RF Base if (device_id == EMSdevice::EMS_DEVICE_ID_RFBASE) { device_type = DeviceType::CONNECT; default_name = "Wireless base"; } + if ((device_id >= EMSdevice::EMS_DEVICE_ID_DHW1 && device_id <= EMSdevice::EMS_DEVICE_ID_DHW8) || device_id == EMSdevice::EMS_DEVICE_ID_IPM_DHW) { device_type = DeviceType::WATER; } @@ -1463,6 +1508,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, const flags = DeviceFlags::EMS_DEVICE_FLAG_CR120; default_name = "CR120"; } + if (product_id == 158 && strncmp(version, "73", 2) == 0) { flags = DeviceFlags::EMS_DEVICE_FLAG_HMC310; default_name = "HMC310"; @@ -1628,6 +1674,7 @@ void EMSESP::reply_own_request(const uint8_t * data, const uint8_t length, const EMSuart::transmit(reply, 7); LOG_DEBUG("Empty reply to 0x%02X read 0x%02X%02X", reply[1], data[5], data[6]); } + return; } @@ -1730,6 +1777,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) { if (poll_id != 0x48 && (uuid::get_uptime_sec() - connect_time) < EMS_WAIT_KM_TIMEOUT) { return; } + wait_km_ = false; // KM200 is polled, from now on it is safe to send } @@ -1797,6 +1845,7 @@ void EMSESP::scheduled_fetch_values() { return; } } + webCustomEntityService.fetch(); no = 0; } @@ -1809,6 +1858,7 @@ void EMSESP::gateway_rx_check(const uint8_t * data, const uint8_t length) { if (length < 4 || (data[0] & 0x7F) != EMSdevice::EMS_DEVICE_ID_GATEWAY1 || !(data[1] & 0x80)) { return; } + uint8_t dest = data[1] & 0x7F; if (dest == 0 || dest == EMSbus::ems_bus_id()) { return; @@ -1819,10 +1869,12 @@ void EMSESP::gateway_rx_check(const uint8_t * data, const uint8_t length) { gateway_window_start_ = now; gateway_window_count_ = 0; } + if (now - gateway_fast_start_ > GATEWAY_FAST_WINDOW) { gateway_fast_start_ = now; gateway_fast_count_ = 0; } + ++gateway_window_count_; ++gateway_fast_count_; if (gateway_window_count_ >= GATEWAY_BURST_THRESHOLD || gateway_fast_count_ >= GATEWAY_FAST_THRESHOLD) { @@ -1840,12 +1892,15 @@ void EMSESP::gateway_tx_hold_start() { if ((int32_t)(now - gateway_cooldown_until_) < 0) { return; } + gateway_cooldown_until_ = 0; } + if (!gateway_hold_until_) { gateway_hold_start_ = now; LOG_INFO("Gateway 0x%02X is busy, holding Tx", EMSdevice::EMS_DEVICE_ID_GATEWAY1); } + gateway_hold_until_ = (now + GATEWAY_HOLD_TIME) | 1; // 0 means no hold } @@ -1853,6 +1908,7 @@ bool EMSESP::gateway_tx_hold() { if (!gateway_hold_until_) { return false; } + uint32_t now = uuid::get_uptime(); if (now - gateway_hold_start_ > GATEWAY_HOLD_MAX) { gateway_hold_until_ = 0; @@ -1860,11 +1916,13 @@ bool EMSESP::gateway_tx_hold() { LOG_WARNING("Gateway 0x%02X still busy after %lu s, resuming Tx", EMSdevice::EMS_DEVICE_ID_GATEWAY1, (unsigned long)(GATEWAY_HOLD_MAX / 1000)); return false; } + if ((int32_t)(now - gateway_hold_until_) >= 0) { gateway_hold_until_ = 0; LOG_INFO("Gateway 0x%02X is quiet, resuming Tx after %lu s", EMSdevice::EMS_DEVICE_ID_GATEWAY1, (unsigned long)((now - gateway_hold_start_) / 1000)); return false; } + return true; } #endif @@ -2044,6 +2102,7 @@ void EMSESP::start() { force_analog_sensors = settings.analogCustomizations.empty(); force_temperature_sensors = settings.sensorCustomizations.empty(); }; + webCustomizationService.read(check_empty_customizations); } } @@ -2052,6 +2111,7 @@ void EMSESP::start() { device_library_ = { #include "device_library.h" }; + LOG_INFO("Library loaded: %d EMS devices, %d device entities, %s", device_library_.size(), EMSESP_TRANSLATION_COUNT, system_.languages_string().c_str()); // start telnet service if it's enabled @@ -2192,10 +2252,12 @@ void EMSESP::loop() { shell_prompt(); show_prompt = false; // only show it once } + int c = serial_console_.read(); if (c != -1) { show_prompt = true; } + // https://daleswanson.org/ascii.htm#:~:text=0 // CTRL-D to open if (c == '\x04') { diff --git a/src/core/emsesp.h b/src/core/emsesp.h index 8bdb6d5b2..6ffcff21d 100644 --- a/src/core/emsesp.h +++ b/src/core/emsesp.h @@ -196,6 +196,7 @@ class EMSESP { static uint8_t watch() { return watch_; } + static void set_read_id(uint16_t id) { read_id_ = id; } @@ -211,6 +212,7 @@ class EMSESP { static bool wait_validate() { return (wait_validate_ != 0); } + static void wait_validate(uint16_t wait) { wait_validate_ = wait; } @@ -328,6 +330,7 @@ class EMSESP { const char * default_name; uint8_t flags; }; + static std::vector> device_library_; static uint16_t watch_id_; diff --git a/src/core/emsesp_network.h b/src/core/emsesp_network.h index 6a9212102..e64ddfe88 100644 --- a/src/core/emsesp_network.h +++ b/src/core/emsesp_network.h @@ -165,16 +165,20 @@ class Network { if (!desc) { return NetIface::NONE; } + if (strcmp(desc, "sta") == 0) { return NetIface::WIFI; } + if (strcmp(desc, "ap") == 0) { return NetIface::AP; } + // any "eth*" (eth, eth0, eth1, ...) is treated as Ethernet if (strncmp(desc, "eth", 3) == 0) { return NetIface::ETHERNET; } + return NetIface::NONE; } @@ -190,6 +194,7 @@ class Network { default: return "None"; } + return "unknown"; } diff --git a/src/core/emsfactory.h b/src/core/emsfactory.h index 72da46200..e8fa76422 100644 --- a/src/core/emsfactory.h +++ b/src/core/emsfactory.h @@ -58,6 +58,7 @@ class EMSFactory { if (auto * ptr = makeRaw(device_type, device_id, product_id, version, default_name, flags, brand)) { return std::unique_ptr(ptr); } + return nullptr; } diff --git a/src/core/firmwareVersion.cpp b/src/core/firmwareVersion.cpp index a63350012..71786ccbf 100644 --- a/src/core/firmwareVersion.cpp +++ b/src/core/firmwareVersion.cpp @@ -55,9 +55,11 @@ static int compare_prerelease(const std::string & a, const std::string & b) { if (a == b) { return 0; } + if (a.empty()) { return 1; // release > prerelease } + if (b.empty()) { return -1; } @@ -70,9 +72,11 @@ static int compare_prerelease(const std::string & a, const std::string & b) { if (ea == std::string::npos) { ea = a.size(); } + if (eb == std::string::npos) { eb = b.size(); } + std::string id_a = a.substr(ia, ea - ia); std::string id_b = b.substr(ib, eb - ib); @@ -102,9 +106,11 @@ static int compare_prerelease(const std::string & a, const std::string & b) { if (ia < a.size()) { return 1; } + if (ib < b.size()) { return -1; } + return 0; } @@ -144,6 +150,7 @@ void FirmwareVersion::parse(const char * s) { if (s == nullptr || *s == '\0') { return; } + // parse numeric major.minor.patch; accept partial ("3", "3.9", "3.9.0") sscanf(s, "%d.%d.%d", &major_, &minor_, &patch_); // capture prerelease tag after '-' if present (stop at '+' which is build metadata) diff --git a/src/core/helpers.cpp b/src/core/helpers.cpp index 0faa6833a..c68dc4d49 100644 --- a/src/core/helpers.cpp +++ b/src/core/helpers.cpp @@ -53,6 +53,7 @@ char * Helpers::hextoa(char * result, const uint16_t value) { if (value <= 0xFF) { return hextoa(result, (uint8_t)value); } + hextoa(result, (uint8_t)(value >> 8)); hextoa(&result[2], (uint8_t)(value & 0xFF)); return result; @@ -201,6 +202,7 @@ char * Helpers::render_boolean(char * result, const bool value, const bool dashb } else { strlcpy(result, value ? "true" : "false", 7); // default } + return result; } @@ -209,10 +211,12 @@ char * Helpers::remove_trailing_zero(char * result) { while (result[strlen(result) - 1] == '0') { result[strlen(result) - 1] = '\0'; } + if (result[strlen(result) - 1] == '.') { result[strlen(result) - 1] = '\0'; } } + return result; } @@ -228,6 +232,7 @@ char * Helpers::render_value(char * result, uint8_t value, int8_t format, const } else { render_boolean(result, true); // assume on. could have value 0x01 or 0xFF } + return result; } @@ -301,6 +306,7 @@ char * Helpers::render_value(char * result, const double value, const int8_t for *result++ = '0'; // add leading zeros } } + itoa(decimal, result, 10); return remove_trailing_zero(ret); @@ -376,6 +382,7 @@ char * Helpers::render_value(char * result, const uint32_t value, const int8_t f if (!hasValue(value)) { return nullptr; } + result[0] = '\0'; uint32_t new_value = fahrenheit ? format ? value * 1.8 + 32 * format * (fahrenheit - 1) : value * 1.8 + 32 * (fahrenheit - 1) : value; char s[14] = {0}; @@ -447,9 +454,11 @@ char * Helpers::render_string(char * result, const char * c, const uint8_t len) *p = (*c & 0x80) ? '?' : *c; break; } + c++; p++; } + *p = '\0'; // terminate result return result; } @@ -489,12 +498,15 @@ char * Helpers::utf8tolatin1(char * result, const char * c, const uint8_t len) { } else { *p = *c; } + c++; p++; } + *p = '\0'; // terminate result return result; } + // creates string of hex values from an array of bytes std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) { if (length == 0) { @@ -512,6 +524,7 @@ std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) { *p++ = buffer[1]; *p++ = ' '; // space } + *--p = '\0'; // null terminate just in case, loosing the trailing space return std::string(str.data()); @@ -559,13 +572,16 @@ int Helpers::atoint(const char * value) { if (s == '-') { ++value; } + while (*value >= '0' && *value <= '9') { x = (x * 10) + (*value - '0'); ++value; } + if (s == '-') { return (-x); } + return x; } @@ -603,6 +619,7 @@ bool Helpers::hasValue(const uint8_t & value, const uint8_t isBool) { if (isBool == EMS_VALUE_BOOL) { return (value != EMS_VALUE_BOOL_NOTSET); } + return (value != EMS_VALUE_UINT8_NOTSET); } @@ -647,6 +664,7 @@ bool Helpers::value2number(const char * value, int & value_i, const int min, con if (value_i >= min && value_i <= max) { return true; } + return false; } @@ -666,6 +684,7 @@ bool Helpers::value2float(const char * value, float & value_f) { value_f = atof(value + 1); return true; } + return false; } @@ -674,8 +693,10 @@ bool Helpers::value2temperature(const char * value, float & value_f, bool relati if (EMSESP::system_.fahrenheit()) { value_f = relative ? (value_f / 1.8) : (value_f - 32) / 1.8; } + return true; } + return false; } @@ -684,8 +705,10 @@ bool Helpers::value2temperature(const char * value, int & value_i, const bool re if (EMSESP::system_.fahrenheit()) { value_i = relative ? (value_i / 1.8) : (value_i - 32) / 1.8; } + return true; } + return false; } @@ -735,6 +758,7 @@ bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * co if ((value == nullptr) || (strlen(value) == 0)) { return false; } + std::string str = toLower(value); for (value_ui = 0; strs[value_ui]; value_ui++) { @@ -746,6 +770,7 @@ bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * co return true; } } + value_ui = 0; return false; } @@ -755,6 +780,7 @@ bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * co if (!value2enum(value, v, strs) || v >= mask.size()) { return false; } + value_ui = mask[v]; return true; } @@ -766,6 +792,7 @@ bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * co if ((value == nullptr) || (strlen(value) == 0)) { return false; } + std::string str = toLower(value); std::string s_on = Helpers::translated_word(FL_(on)); @@ -791,6 +818,7 @@ bool Helpers::value2enum(const char * value, uint8_t & value_ui, const char * co if (!value2enum(value, v, strs) || v >= mask.size()) { return false; } + value_ui = mask[v]; return true; } @@ -829,6 +857,7 @@ void Helpers::CharToUpperUTF8(char * c) { if ((p_v >= (char)0xA0) && (p_v <= (char)0xBE)) { *p -= 0x20; } + // Additional special characters for supported languages switch (p_v) { case (char)0xA0: // à -> À @@ -865,6 +894,7 @@ void Helpers::CharToUpperUTF8(char * c) { *p -= 0x20; break; } + break; case (char)0xC4: switch (p_v) { @@ -881,6 +911,7 @@ void Helpers::CharToUpperUTF8(char * c) { *p -= 1; break; } + break; case (char)0xC5: switch (p_v) { @@ -903,6 +934,7 @@ void Helpers::CharToUpperUTF8(char * c) { *p -= 1; break; } + break; default: *c = toupper(*c); // works on Latin1 letters @@ -939,6 +971,7 @@ uint8_t Helpers::count_items(const char * const * list) { list_size++; } } + return list_size; } @@ -951,6 +984,7 @@ uint8_t Helpers::count_items(const char * const ** list) { list_size++; } } + return list_size; } @@ -983,19 +1017,23 @@ uint16_t Helpers::string2minutes(const std::string & str) { if (str[i] >= '0' && str[i] <= '9') { tmp = tmp * 10 + str[i] - '0'; } + // Or if we got a colon else if (str[i] == ':') { // If we were reading the hours if (state == 0) { res = 60 * tmp; } + // Or if we were reading the minutes else if (state == 1) { if (tmp > 60) { return 0; } + res += tmp; } + // Or we got an extra colon else { return 0; @@ -1009,6 +1047,7 @@ uint16_t Helpers::string2minutes(const std::string & str) { else { return 0; } + i++; } @@ -1034,6 +1073,7 @@ int16_t Helpers::calc_dew(int16_t temp, uint8_t humi) { if (humi == EMS_VALUE_UINT8_NOTSET || temp == EMS_VALUE_INT16_NOTSET) { return EMS_VALUE_INT16_NOTSET; } + const float k2 = 17.62; const float k3 = 243.12; const float t = (float)temp / 10; diff --git a/src/core/httpClient.cpp b/src/core/httpClient.cpp index cf2a104c0..77c92bb05 100644 --- a/src/core/httpClient.cpp +++ b/src/core/httpClient.cpp @@ -69,6 +69,7 @@ int HttpClient::request(std::string url, const std::string & method, const std:: ssl_client->setBufferSizes(TLS_RX_BUFFER_SIZE, TLS_TX_BUFFER_SIZE); ssl_client->setSessionTimeout(TLS_SESSION_TIMEOUT_S); } + // WiFiClient is NetworkClient, which declares no setTimeout() of its own - calling it binds to // Stream::setTimeout() and only affects readBytes(), leaving the socket on the core's 3s default basic_client->setConnectionTimeout(CONNECT_TIMEOUT_MS); @@ -89,17 +90,21 @@ int HttpClient::request(std::string url, const std::string & method, const std:: agent_set |= (key == "user-agent"); req += std::string(p.key().c_str()) + ": " + p.value().as() + "\r\n"; } + if (!agent_set) { req += "User-Agent: EMS-ESP\r\n"; // CDNs are more likely to stall on a request without one } + if (is_post) { if (!content_set) { req += "Content-Type: "; req += value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain; req += "\r\n"; } + req += "Content-Length: " + std::to_string(value.length()) + "\r\n"; } + req += "Connection: close\r\n\r\n"; // the blank line terminates the headers - without it the server never responds ssl_client->print(req.c_str()); @@ -120,6 +125,7 @@ int HttpClient::request(std::string url, const std::string & method, const std:: truncated = true; break; } + uint8_t buf[128]; const size_t want = (avail < (int)sizeof(buf)) ? (size_t)avail : sizeof(buf); const int len = ssl_client->read(buf, want); @@ -128,20 +134,25 @@ int HttpClient::request(std::string url, const std::string & method, const std:: last_data = millis(); continue; } + // available() reported bytes the client won't hand over. Yielding here keeps a // stale count from busy-spinning the whole read budget on this task's core delay(1); continue; } + if (!ssl_client->connected()) { break; // closed, with nothing left buffered } + // the server may take a while to start replying if (millis() - last_data > (result.empty() ? FIRST_BYTE_TIMEOUT_MS : IDLE_TIMEOUT_MS)) { break; } + delay(1); } + ssl_client->stop(); const auto received = result.length(); @@ -170,6 +181,7 @@ int HttpClient::request(std::string url, const std::string & method, const std:: } else { EMSESP::logger().warning("%s connection to %s failed", is_https ? "HTTPS" : "HTTP", host.c_str()); } + delete ssl_client; delete basic_client; diff --git a/src/core/led.cpp b/src/core/led.cpp index cbe068c3a..69a3c08f1 100644 --- a/src/core/led.cpp +++ b/src/core/led.cpp @@ -59,6 +59,7 @@ Channel * init(uint8_t pin) { if (ch.pin == pin) { return &ch; } + if (!free_slot && ch.pin == NO_PIN) { free_slot = &ch; } @@ -73,12 +74,14 @@ Channel * init(uint8_t pin) { free_slot = &ch; } } + end(free_slot->pin); } if (!rmtInit(pin, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, RMT_FREQ_HZ)) { return nullptr; } + free_slot->pin = pin; free_slot->last_tx_us = micros() - RESET_US; // no wait on first write return free_slot; @@ -188,6 +191,7 @@ bool LED::loop(uint8_t healthcheck, bool button_busy) { } else { reset_led(); } + return false; } @@ -267,6 +271,7 @@ void LED::set_led(Color color) { if (!led_gpio_ || color == last_color_) { return; } + last_color_ = color; // RGB lookup table indexed by Color enum (must match enum order in led.h) @@ -283,6 +288,7 @@ void LED::set_led(Color color) { {0, B, B}, // CYAN {H, 0, H} // PINK }; + static_assert(sizeof(rgb_table) / sizeof(rgb_table[0]) == Color::PINK + 1, "rgb_table must match the Color enum"); const uint8_t * rgb = rgb_table[color]; @@ -324,6 +330,7 @@ bool LED::set_custom_led_routine(const std::string & color, const std::string & break; } } + if (!color_matched) { return false; } @@ -393,6 +400,7 @@ void LED::sequence_led() { is_user_led_blink_ = false; previous_healthcheck_ = System::HEALTHCHECK_RESET; // this will force the healthcheck to be checked again } + return; } @@ -414,4 +422,4 @@ void LED::start_led_fast_flash(uint8_t duration) { led_fast_flash_timer_ = true; // it's active } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/core/littlefs_compat.cpp b/src/core/littlefs_compat.cpp index 6a0b63071..0dd466f55 100644 --- a/src/core/littlefs_compat.cpp +++ b/src/core/littlefs_compat.cpp @@ -72,21 +72,27 @@ constexpr uint32_t TYPE_CCRC = 0x500; constexpr bool tag_isvalid(uint32_t t) { return (t & 0x80000000) == 0; } + constexpr uint32_t tag_type2(uint32_t t) { return (t & 0x78000000) >> 20; } + constexpr uint32_t tag_type3(uint32_t t) { return (t & 0x7ff00000) >> 20; } + constexpr uint32_t tag_chunk(uint32_t t) { return (t & 0x0ff00000) >> 20; } + constexpr uint32_t tag_id(uint32_t t) { return (t & 0x000ffc00) >> 10; } + constexpr uint32_t tag_size(uint32_t t) { return t & 0x3ff; } + // a size of 0x3ff marks a delete tag, which carries no payload constexpr uint32_t tag_dsize(uint32_t t) { return 4 + (tag_size(t) == 0x3ff ? 0 : tag_size(t)); @@ -95,9 +101,11 @@ constexpr uint32_t tag_dsize(uint32_t t) { uint32_t rd_be32(const uint8_t * p) { return (static_cast(p[0]) << 24) | (static_cast(p[1]) << 16) | (static_cast(p[2]) << 8) | p[3]; } + uint32_t rd_le32(const uint8_t * p) { return (static_cast(p[3]) << 24) | (static_cast(p[2]) << 16) | (static_cast(p[1]) << 8) | p[0]; } + void wr_le32(uint8_t * p, uint32_t v) { p[0] = static_cast(v); p[1] = static_cast(v >> 8); @@ -125,11 +133,13 @@ uint32_t lfs_crc(uint32_t crc, const void * buffer, size_t size) { 0xa00ae278, 0xbdbdf21c, }; + const uint8_t * data = static_cast(buffer); for (size_t i = 0; i < size; i++) { crc = (crc >> 4) ^ rtable[(crc ^ data[i]) & 0xf]; crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 4)) & 0xf]; } + return crc; } @@ -166,15 +176,18 @@ Located locate_superblock(const uint8_t * buf) { if (!tag_isvalid(tag)) { break; // nothing committed beyond this point } + if (off + tag_dsize(tag) > BLOCK_SIZE) { break; } + ptag = tag; if (tag_type2(tag) == TYPE_CCRC) { if (crc != rd_le32(&buf[off + 4])) { break; // commit never completed, so the chain ends here } + if (pending) { found.found = true; found.struct_off = pending_off; @@ -182,6 +195,7 @@ Located locate_superblock(const uint8_t * buf) { found.crc_off = off + 4; pending = false; } + ptag ^= (tag_chunk(tag) & 1u) << 31; crc = 0xffffffff; commit_start = off + tag_dsize(tag); // the padding after the CRC is not covered @@ -204,11 +218,13 @@ const esp_partition_t * find_partition(const char * partition_label) { if (partition_label != nullptr) { return esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, partition_label); } + // 0x83 is the littlefs subtype, 0x82 the spiffs one that older partition tables use const esp_partition_t * part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, static_cast(0x83), nullptr); if (part != nullptr) { return part; } + return esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, nullptr); } diff --git a/src/core/modbus.cpp b/src/core/modbus.cpp index bee33b445..90f2c2244 100644 --- a/src/core/modbus.cpp +++ b/src/core/modbus.cpp @@ -37,12 +37,14 @@ void Modbus::start(uint8_t systemServerId, uint16_t port, uint8_t max_clients, u modbusServer_->registerWorker(i, WRITE_MULT_REGISTERS, [this](auto && request) { return handleWrite(request); }); } } + modbusServer_->start(port, max_clients, timeout); LOG_INFO("Starting Modbus service (ID %d, port %d)", systemServerId, port); #else if (!check_parameter_order()) { LOG_ERROR("Unable to enable Modbus - the parameter list order is corrupt. This is a firmware bug."); } + LOG_INFO("Modbus deactivated in standalone build."); #endif } @@ -71,8 +73,10 @@ bool Modbus::check_parameter_order() { LOG_ERROR("Error in modbus parameters: %s must be listed before %s.", mi.short_name, prev->short_name); return false; } + prev = &mi; } + return true; } @@ -263,10 +267,12 @@ ModbusMessage Modbus::handleRead(const ModbusMessage & request) { } } } + if (error_code) { if (uuid::get_uptime_sec() > 60 || error_code < -2) { // suppress not found messages for the first minute LOG_ERROR("Unable to read raw device value %s for tag=%d - error_code = %d", modbusInfo->short_name, (int)tag, error_code); } + response.setError(request.getServerID(), request.getFunctionCode(), SERVER_DEVICE_FAILURE); return response; } @@ -361,6 +367,7 @@ ModbusMessage Modbus::handleWrite(const ModbusMessage & request) { } } } + if (error_code) { // error getting modbus value as json LOG_ERROR("error getting modbus value as json, error code = %d", error_code); @@ -397,6 +404,7 @@ ModbusMessage Modbus::handleWrite(const ModbusMessage & request) { } else { snprintf(error, sizeof(error), "Modbus write command failed with error code (%s)", Command::return_code_string(return_code)); } + LOG_ERROR(error); response.setError(request.getServerID(), request.getFunctionCode(), ILLEGAL_DATA_VALUE); } else { diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index c4dffdf39..576a7c93d 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -83,6 +83,7 @@ void Mqtt::subscribe(const uint8_t device_type, const std::string & topic, mqtt_ if (cb) { mqtt_subfunction.mqtt_subfunction_ = cb; } + return; // exit - don't add } } @@ -237,6 +238,7 @@ void Mqtt::on_message(const char * topic, const uint8_t * payload, size_t len) { queue_publish_message(topic, "", true); LOG_DEBUG("Remove topic %s", topic); } + return; } @@ -251,6 +253,7 @@ void Mqtt::on_message(const char * topic, const uint8_t * payload, size_t len) { LOG_ERROR("error: invalid payload %s for this topic %s", message, topic); Mqtt::queue_publish("response", "error: invalid data"); } + return; } } @@ -282,6 +285,7 @@ void Mqtt::on_message(const char * topic, const uint8_t * payload, size_t len) { } else { snprintf(error, sizeof(error), "MQTT command failed with error %s", Command::return_code_string(return_code)); } + LOG_ERROR(error); Mqtt::queue_publish("response", error); } else { @@ -351,6 +355,7 @@ void Mqtt::reset_mqtt() { if (mqttClient_->connected()) { mqttClient_->disconnect(true); // force a disconnect } + load_settings(); // reload MQTT settings } @@ -401,6 +406,7 @@ void Mqtt::start() { if (initialized_) { return; } + initialized_ = true; // add the 'publish' command ('call system publish' in console or via API) @@ -447,6 +453,7 @@ bool Mqtt::get_publish_onchange(uint8_t device_type) { if (publish_single_ && !ha_enabled_) { return false; } + if (device_type == EMSdevice::DeviceType::BOILER) { if (!publish_time_boiler_) { return true; @@ -470,6 +477,7 @@ bool Mqtt::get_publish_onchange(uint8_t device_type) { } else if (!publish_time_other_) { return true; } + return false; } @@ -626,6 +634,7 @@ void Mqtt::ha_status() { "bootTime", DeviceValueUOM::UPTIME); // called Uptime in HA and derives value from bootTime element in info topic } + // add sub or pub task to the queue. // the base is not included in the topic bool Mqtt::queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain) { @@ -648,14 +657,17 @@ bool Mqtt::queue_message(const uint8_t operation, const std::string & topic, con mqtt_message_id_++; mqtt_publish_fails_++; } + LOG_WARNING("%s failed: low memory", operation == Operation::PUBLISH ? "Publish" : operation == Operation::SUBSCRIBE ? "Subscribe" : "Unsubscribe"); return false; // quit } + if (queuecount_ >= MQTT_QUEUE_MAX_SIZE) { if (operation == Operation::PUBLISH) { mqtt_message_id_++; mqtt_publish_fails_++; } + LOG_WARNING("%s failed: queue full", operation == Operation::PUBLISH ? "Publish" : operation == Operation::SUBSCRIBE ? "Subscribe" : "Unsubscribe"); return false; // quit } @@ -758,6 +770,7 @@ bool Mqtt::queue_publish(const char * topic, const JsonObjectConst payload, cons serializeJson(payload, payload_text); // convert json to string return queue_publish_message(topic, payload_text, retain); } + return false; } @@ -893,6 +906,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev snprintf(entity_with_tag, sizeof(entity_with_tag), "%sww", dhw_old[i]); } } + snprintf(uniq_id, sizeof(uniq_id), "%s_%s", device_name, entity_with_tag); } else if (has_tag && device_type == EMSdevice::DeviceType::WATER && tag >= DeviceValue::DeviceValueTAG::TAG_DHW3) { snprintf(entity_with_tag, sizeof(entity_with_tag), "wwc%d_%s", tag - DeviceValue::DeviceValueTAG::TAG_DHW1 + 1, entity); @@ -913,6 +927,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev snprintf(entity_with_tag, sizeof(entity_with_tag), "%sww", dhw_old[i]); } } + snprintf(uniq_id, sizeof(uniq_id), "%s_%s_%s", Mqtt::basename().c_str(), device_name, entity_with_tag); } else if (has_tag && device_type == EMSdevice::DeviceType::WATER && tag >= DeviceValue::DeviceValueTAG::TAG_DHW3) { snprintf(entity_with_tag, sizeof(entity_with_tag), "wwc%d_%s", tag - DeviceValue::DeviceValueTAG::TAG_DHW1 + 1, entity); @@ -975,6 +990,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev if (discovery_type() == discoveryType::HOMEASSISTANT || discovery_type() == discoveryType::DOMOTICZ_LATEST) { snprintf(topic, sizeof(topic), "number/%s", config_topic); } + break; case DeviceValueType::BOOL: // switch - https://www.home-assistant.io/integrations/switch.mqtt @@ -990,6 +1006,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev } else if (discovery_type() == discoveryType::HOMEASSISTANT || discovery_type() == discoveryType::DOMOTICZ_LATEST) { snprintf(topic, sizeof(topic), "number/%s", config_topic); } + break; case DeviceValueType::STRING: // text - https://www.home-assistant.io/integrations/text.mqtt @@ -997,6 +1014,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev if (discovery_type() == discoveryType::HOMEASSISTANT) { snprintf(topic, sizeof(topic), "text/%s", config_topic); // e.g. set_datetime, set_holiday, set_wwswitchtime } + break; default: break; @@ -1040,6 +1058,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev } else { snprintf(command_topic, sizeof(command_topic), "~/%s/%s", device_name, entity); } + doc["cmd_t"] = command_topic; // extend for enums and also commands, add options @@ -1063,6 +1082,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev } else { doc["mode"] = "box"; // auto, slider or box } + if (num_op > 0) { doc["step"] = 1.0 / num_op; } else if (num_op < 0) { @@ -1089,6 +1109,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev } else { snprintf(ha_name, sizeof(ha_name), "%s", F_name); // no tag } + free(F_name); // very important! doc["name"] = ha_name; @@ -1108,6 +1129,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info)); } } + doc["stat_t"] = stat_t; // value template @@ -1141,6 +1163,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // default doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else 'None'}}"; } + add_ha_avty_section(doc.as(), stat_t, val_cond); // adds availability section } else { // Domoticz doesn't support value templates, so we just use the value directly @@ -1180,6 +1203,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 if (device_type == EMSdevice::DeviceType::SYSTEM) { doc["ent_cat"] = "diagnostic"; // instead of 'config' } + if (type == DeviceValueType::BOOL) { return; } @@ -1238,12 +1262,14 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 if (display_only) { doc[ic_ha] = F_(iconpercent); // set icon } + doc[sc_ha] = sc_ha_measurement; if (entity == FL_(airHumidity)[0] || entity == FL_(remotehum)[0]) { doc[dc_ha] = "humidity"; } else if (entity == FL_(battery)[0]) { doc[dc_ha] = "battery"; } + break; case DeviceValueUOM::SECONDS: case DeviceValueUOM::MINUTES: @@ -1253,12 +1279,14 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 } else { doc[sc_ha] = sc_ha_measurement; } + doc[dc_ha] = "duration"; // https://github.com/emsesp/EMS-ESP32/issues/822 break; case DeviceValueUOM::KB: if (display_only) { doc[ic_ha] = F_(iconkb); } + doc[dc_ha] = "data_size"; break; case DeviceValueUOM::LMIN: @@ -1266,6 +1294,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 if (display_only) { doc[ic_ha] = F_(iconlmin); } + doc[sc_ha] = sc_ha_measurement; doc[dc_ha] = "volume_flow_rate"; break; @@ -1280,6 +1309,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 } else { doc[sc_ha] = sc_ha_measurement; // no device class for this } + break; case DeviceValueUOM::KWH: doc[sc_ha] = sc_ha_total_increasing; @@ -1289,6 +1319,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 if (display_only) { doc[ic_ha] = F_(iconua); } + doc[sc_ha] = sc_ha_measurement; // doc[dc_ha] = "current"; // there is no uA in HA, only mA break; @@ -1354,6 +1385,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 if (display_only) { doc[ic_ha] = F_(iconnum); // set icon } + // determine if its a measurement or total increasing // most of the values are measurement. for example Tx Reads will increment but can be reset to 0 after a restart // all the starts are increasing, and they are ULONGs @@ -1363,6 +1395,7 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8 doc[sc_ha] = sc_ha_measurement; // default to measurement } } + break; } } @@ -1412,10 +1445,12 @@ bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_room snprintf(currtemp_s, sizeof(currtemp_s), "value_json.%s%d.currtemp", tagname, hc_num); snprintf(currtemp_cond, sizeof(currtemp_cond), "value_json.%s%d is defined and %s is defined", tagname, hc_num, currtemp_s); } + if (tag >= DeviceValueTAG::TAG_SRC1) { snprintf(currhum_s, sizeof(currhum_s), "value_json.%s%d.airhumidity", tagname, hc_num); snprintf(currhum_cond, sizeof(currhum_cond), "value_json.%s%d is defined and %s is defined", tagname, hc_num, currhum_s); } + snprintf(topic_t, sizeof(topic_t), "~/%s", Mqtt::tag_to_topic(device_type, DeviceValueTAG::TAG_NONE).c_str()); } else { // single format @@ -1428,10 +1463,12 @@ bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_room snprintf(currtemp_s, sizeof(currtemp_s), "value_json.currtemp"); snprintf(currtemp_cond, sizeof(currtemp_cond), "%s is defined", currtemp_s); } + if (tag >= DeviceValueTAG::TAG_SRC1) { snprintf(currhum_s, sizeof(currhum_s), "value_json.airhumidity"); snprintf(currhum_cond, sizeof(currhum_cond), "%s is defined", currhum_s); } + snprintf(topic_t, sizeof(topic_t), "~/%s", Mqtt::tag_to_topic(device_type, tag).c_str()); } @@ -1480,6 +1517,7 @@ bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_room doc["curr_temp_t"] = topic_t; doc["curr_temp_tpl"] = (std::string) "{{" + currtemp_s + " if " + currtemp_cond + " else 'None'}}"; } + if (tag >= DeviceValueTAG::TAG_SRC1) { doc["curr_hum_t"] = topic_t; doc["curr_hum_tpl"] = (std::string) "{{" + currhum_s + " if " + currhum_cond + " else 'None'}}"; @@ -1531,12 +1569,15 @@ bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_room if (found_auto) { modes.add("auto"); } + if (found_heat) { modes.add("heat"); } + if (found_off) { modes.add("off"); } + if (found_cool) { modes.add("cool"); } diff --git a/src/core/mqtt.h b/src/core/mqtt.h index 5bbf9f821..873885e32 100644 --- a/src/core/mqtt.h +++ b/src/core/mqtt.h @@ -164,6 +164,7 @@ class Mqtt { if (discovery_prefix_.empty()) { return std::string{}; } + return discovery_prefix_ + "/"; } @@ -334,4 +335,4 @@ class Mqtt { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/core/network.cpp b/src/core/network.cpp index 8dbeccb8f..eb58ee7f6 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -151,6 +151,7 @@ bool Network::formatBSSID([[maybe_unused]] const String & bssid, [[maybe_unused] if (bssid.isEmpty() || sscanf(bssid.c_str(), "%X:%X:%X:%X:%X:%X", &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5]) != 6) { return false; } + for (uint8_t i = 0; i < 6; i++) { mac[i] = static_cast(tmp[i]); } @@ -187,6 +188,7 @@ std::vector Network::ipv6_addresses([[maybe_unused]] Netwo esp_ip6_addr_type_t type; const char * name; }; + static const Scope scopes[] = {{ESP_IP6_ADDR_IS_GLOBAL, "global"}, {ESP_IP6_ADDR_IS_UNIQUE_LOCAL, "unique local"}, {ESP_IP6_ADDR_IS_SITE_LOCAL, "site local"}, @@ -216,6 +218,7 @@ std::vector Network::dns_servers(const NetworkInterface & netif) { servers.push_back(ip); } } + return servers; } @@ -236,9 +239,11 @@ esp_err_t collect_ipv6_gateways(void * ctx) { if (router == nullptr || netif_get_index(router->netif) != args->netif_index) { continue; } + const ip6_addr_t * addr = &router->next_hop_address; args->gateways->push_back(IPAddress(IPv6, reinterpret_cast(addr->addr), ip6_addr_zone(addr))); } + return ESP_OK; } @@ -343,6 +348,7 @@ NetPhase Network::initialPhase() const { if (!ssid_.isEmpty() || ap_provisionMode_ == AP_MODE_NEVER) { return NetPhase::WIFI; } + return NetPhase::AP; } @@ -354,6 +360,7 @@ void Network::loop() { reconnect_pending_ = false; reconnect(); } + // already have a connection: verify it's still alive if (network_ip_ != 0 || last_disconnect_reason_ != 0) { checkConnection(); @@ -394,10 +401,12 @@ void Network::checkConnection() { if (iface_from_desc(esp_netif_get_desc(netif)) != network_iface_) { continue; } + esp_netif_ip_info_t ip_info = {}; if (esp_netif_is_netif_up(netif) && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK && ip_info.ip.addr == network_ip_) { still_up = true; } + break; // only one active netif per kind in our world (sta / eth / ap) } @@ -416,8 +425,10 @@ void Network::checkConnection() { if (wifi_radio_off_) { startWiFiRadio(); } + return; } + begin(); } #endif @@ -439,6 +450,7 @@ void Network::setWiFiPower([[maybe_unused]] uint8_t tx_power) { if (!WiFi.setTxPower(static_cast(tx_power))) { emsesp::EMSESP::logger().warning("Failed to set WiFi Tx Power"); } + return; } @@ -455,6 +467,7 @@ void Network::setWiFiPower([[maybe_unused]] uint8_t tx_power) { if (newrssi < threshold) { min_tx_pwr = threshold - newrssi; } + if (min_tx_pwr > max_tx_pwr) { min_tx_pwr = max_tx_pwr; } @@ -621,6 +634,7 @@ void Network::startWiFiRadio() { } else { esp_wifi_set_bandwidth(static_cast(ESP_IF_WIFI_STA), WIFI_BW_HT40); } + if (nosleep_) { WiFi.setSleep(false); // turn off sleep - WIFI_PS_NONE } else { @@ -638,6 +652,7 @@ void Network::stopWiFiRadio() { if (wifi_radio_off_) { return; } + WiFi.mode(WIFI_OFF); wifi_radio_off_ = true; wifi_connect_pending_ = false; @@ -801,18 +816,23 @@ const char * Network::ethernet_status() const { if (phy_type_ == PHY_type::PHY_TYPE_NONE) { return nullptr; } + if (ethClockConflictsPsram()) { return "disabled - clock mode GPIO16/17 conflicts with PSRAM"; } + if (ethernet_connected()) { return "connected"; } + if (ethernet_init_failed_) { return "failed to initialize - no PHY found, check the board profile"; } + if (!ethernet_started_) { return "not started"; } + return ethernet_connect_pending_ ? "connecting" : "no connection"; #else return nullptr; @@ -871,6 +891,7 @@ void Network::findNetworks() { if (desc) { strlcpy(info.desc, desc, sizeof(info.desc)); } + info.has_ipv6 = (esp_netif_get_ip6_linklocal(netif, &info.ip6) == ESP_OK); best_iface = candidate; @@ -896,9 +917,11 @@ void Network::findNetworks() { } else if (network_iface_ == NetIface::AP) { phase_ = NetPhase::AP; } + if (!ethernet_ever_connected_ && ethernet_connected()) { ethernet_ever_connected_ = true; } + LOG_INFO("Network connected via %s (IP: " IPSTR ")", network_iface_ == NetIface::ETHERNET ? "Ethernet" : network_iface_ == NetIface::WIFI ? "WiFi" @@ -962,6 +985,7 @@ void Network::findNetworks() { LOG_WARNING("Ethernet failed to connect after %u attempts, switching to WiFi", connect_retry_); phase_ = NetPhase::WIFI; } + ethernet_connect_pending_ = false; } else if (phase_ == NetPhase::WIFI) { if (!wifi_ever_connected_) { @@ -972,8 +996,10 @@ void Network::findNetworks() { phase_ = NetPhase::AP; } } + wifi_connect_pending_ = false; } + connect_retry_ = 0; } } @@ -1009,6 +1035,7 @@ void Network::startAP() { LOG_DEBUG("softAPConfig failed"); return; } + esp_wifi_set_bandwidth(static_cast(ESP_IF_WIFI_AP), WIFI_BW_HT20); // WiFi.softAPenableIPv6(); @@ -1027,6 +1054,7 @@ void Network::startAP() { WiFi.softAPdisconnect(true); return; } + LOG_INFO("Starting Access Point with captive portal on %u.%u.%u.%u", apIp[0], apIp[1], apIp[2], apIp[3]); ap_dnsServer_ = new DNSServer; @@ -1035,6 +1063,7 @@ void Network::startAP() { WiFi.softAPdisconnect(true); return; } + ap_dnsServer_->start(DNS_PORT, "*", apIp); #endif } diff --git a/src/core/psram_async_json_response.h b/src/core/psram_async_json_response.h index 0d789bd54..8478ced94 100644 --- a/src/core/psram_async_json_response.h +++ b/src/core/psram_async_json_response.h @@ -81,6 +81,7 @@ class PsramAsyncJsonResponse : public ::AsyncJsonResponse { if (_contentLength) { _isValid = true; } + return _contentLength; } @@ -119,6 +120,7 @@ class PsramAsyncMessagePackResponse : public ::AsyncMessagePackResponse { if (_contentLength) { _isValid = true; } + return _contentLength; } diff --git a/src/core/psram_json_allocator.h b/src/core/psram_json_allocator.h index a6b6a4b01..b4abbac15 100644 --- a/src/core/psram_json_allocator.h +++ b/src/core/psram_json_allocator.h @@ -57,6 +57,7 @@ class PsramJsonAllocator : public ArduinoJson::Allocator { if (p == nullptr) { p = malloc(size); } + return p; #endif } @@ -80,6 +81,7 @@ class PsramJsonAllocator : public ArduinoJson::Allocator { if (p == nullptr) { p = realloc(ptr, new_size); } + return p; #endif } diff --git a/src/core/roomcontrol.cpp b/src/core/roomcontrol.cpp index 046f4154a..71d5aa903 100644 --- a/src/core/roomcontrol.cpp +++ b/src/core/roomcontrol.cpp @@ -36,10 +36,12 @@ uint32_t Roomctrl::timeout_ = 0; void Roomctrl::set_timeout(uint8_t t) { timeout_ = t * 3600000; // ms } + void Roomctrl::set_remotetemp(const uint8_t type, const uint8_t hc, const int16_t temp) { if (!type_[hc] && !type) { return; } + if (remotetemp_[hc] != EMS_VALUE_INT16_NOTSET && temp == EMS_VALUE_INT16_NOTSET) { // switch remote off remotetemp_[hc] = EMS_VALUE_INT16_NOTSET; switch_off_[hc] = true; @@ -47,13 +49,16 @@ void Roomctrl::set_remotetemp(const uint8_t type, const uint8_t hc, const int16_ sendtype_[hc] = SendType::TEMP; return; } + if (hc >= HCS || !type) { return; } + if (remotetemp_[hc] != temp) { send_time_[hc] = uuid::get_uptime() - SEND_INTERVAL; // send now sendtype_[hc] = SendType::TEMP; } + type_[hc] = type; remotetemp_[hc] = temp; receive_time_[hc] = uuid::get_uptime(); @@ -64,10 +69,12 @@ void Roomctrl::set_remotehum(const uint8_t type, const uint8_t hc, const int8_t if (hc >= HCS || type != type_[hc]) { return; } + if (remotehum_[hc] != hum) { send_time_[hc] = uuid::get_uptime() - SEND_INTERVAL; // send now sendtype_[hc] = SendType::HUMI; } + remotehum_[hc] = hum; } @@ -82,6 +89,7 @@ uint8_t Roomctrl::get_hc(uint8_t addr) { } else if (addr >= 0x18 && addr <= 0x1B && (type_[addr - 0x18] == RC20 || type_[addr - 0x18] == FB10)) { return addr - 0x18; // RC20, FB10 } + return 0xFF; // invalid } @@ -92,11 +100,13 @@ void Roomctrl::send(uint8_t addr) { if ((addr & 0x80) || EMSESP::system_.readonly_mode()) { return; } + uint8_t hc = get_hc(addr); // check address, reply only on addresses 0x18..0x1B or 0x40..0x43 if (hc >= HCS) { return; } + // no reply if the temperature is not set if (!switch_off_[hc] && remotetemp_[hc] == EMS_VALUE_INT16_NOTSET && remotehum_[hc] == EMS_VALUE_UINT8_NOTSET) { return; @@ -108,12 +118,14 @@ void Roomctrl::send(uint8_t addr) { sendtype_[hc] = SendType::TEMP; EMSESP::logger().warning("remotetemp timeout hc%d, stop sending roomtemperature to thermostat", hc); } + if (switch_off_[hc] || (uuid::get_uptime() - send_time_[hc]) > SEND_INTERVAL) { // check interval if (type_[hc] == RC100H || type_[hc] == RT800 || type_[hc] == RC120RF) { if (sendtype_[hc] == SendType::HUMI) { // send humidity if (switch_off_[hc]) { remotehum_[hc] = EMS_VALUE_UINT8_NOTSET; } + send_time_[hc] = uuid::get_uptime(); humidity(addr, 0x10, hc); sendtype_[hc] = SendType::TEMP; @@ -123,6 +135,7 @@ void Roomctrl::send(uint8_t addr) { } else { send_time_[hc] = uuid::get_uptime(); } + temperature(addr, 0x10, hc); // send to master-thermostat } } else if (type_[hc] == RC200 || type_[hc] == RC100 || type_[hc] == FB10) { @@ -132,6 +145,7 @@ void Roomctrl::send(uint8_t addr) { send_time_[hc] = uuid::get_uptime(); temperature(addr, 0x00, hc); // send to all } + if (remotehum_[hc] == EMS_VALUE_UINT8_NOTSET && switch_off_[hc]) { switch_off_[hc] = false; type_[hc] = RemoteType::NONE; @@ -149,22 +163,27 @@ void Roomctrl::check(uint8_t addr, const uint8_t * data, const uint8_t length) { if (EMSESP::system_.readonly_mode()) { return; } + uint8_t hc = get_hc(addr); if (hc >= HCS || length < 5) { return; } + if (type_[hc] == SENSOR) { return; } + // no reply if the temperature is not set if (remotetemp_[hc] == EMS_VALUE_INT16_NOTSET) { return; } + // reply to writes with write nack byte if ((addr & 0x80) == 0) { // it's a write to us ack_write(); // accept writes, don't care. return; } + addr &= 0x7F; // reads: for now we only reply to version and remote temperature // empty message back if temperature not set or unknown message type @@ -382,6 +401,7 @@ void Roomctrl::humidity(uint8_t addr, uint8_t dst, uint8_t hc) { if (dew != EMS_VALUE_INT16_NOTSET) { dew8 = static_cast((dew >= 0 ? dew + 5 : dew - 5) / 10); } + uint8_t data[11]; data[0] = addr | EMSbus::ems_mask(); data[1] = dst & 0x7F; @@ -436,6 +456,7 @@ void Roomctrl::replyF7(uint8_t addr, uint8_t dst, uint8_t offset, uint8_t typehh data[7] = 0; data[8] = 0; } + data[9] = EMSbus::calculate_crc(data, 9); // append CRC EMSuart::transmit(data, 10); } diff --git a/src/core/roomcontrol.h b/src/core/roomcontrol.h index e85f3b5c6..338bbc237 100644 --- a/src/core/roomcontrol.h +++ b/src/core/roomcontrol.h @@ -34,6 +34,7 @@ class Roomctrl { static bool is_remote(const uint8_t hc) { return (hc < 4 && remotetemp_[hc] != EMS_VALUE_INT16_NOTSET); } + static void set_timeout(uint8_t t); private: @@ -63,4 +64,4 @@ class Roomctrl { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/core/shower.cpp b/src/core/shower.cpp index 3161cc46c..004cd3d8e 100644 --- a/src/core/shower.cpp +++ b/src/core/shower.cpp @@ -46,6 +46,7 @@ void Shower::start() { LOG_WARNING("Coldshot failed. Shower not active"); force_coldshot = false; } + return true; }, FL_(coldshot_cmd), @@ -90,6 +91,7 @@ void Shower::loop() { set_shower_state(true); LOG_DEBUG("hot water still running, starting shower timer"); } + // check if the shower has been on too long else if ((shower_alert_ && ((time_now - timer_start_) > next_alert_)) || force_coldshot) { shower_alert_start(); @@ -138,6 +140,7 @@ void Shower::loop() { set_shower_state(false); } } + return; } @@ -178,6 +181,7 @@ void Shower::set_shower_state(bool state, bool force) { if ((shower_state_ == old_shower_state_) && !force) { return; } + old_shower_state_ = shower_state_; // always publish as a string - see https://github.com/emsesp/EMS-ESP/issues/369 @@ -205,6 +209,7 @@ void Shower::create_ha_discovery() { } else { snprintf(str, sizeof(str), "shower_active"); // v3.4 compatible } + doc["uniq_id"] = str; doc["def_ent_id"] = (std::string) "binary_sensor." + str; doc["stat_t"] = "~/shower_active"; diff --git a/src/core/shower.h b/src/core/shower.h index 15a4b8264..dc7ba079c 100644 --- a/src/core/shower.h +++ b/src/core/shower.h @@ -41,6 +41,7 @@ class Shower { void shower_alert(bool enable) { shower_alert_ = enable; } + void ha_reset() { ha_configdone_ = false; } diff --git a/src/core/shuntingYard.cpp b/src/core/shuntingYard.cpp index 78a5fed2c..9ecae4f71 100644 --- a/src/core/shuntingYard.cpp +++ b/src/core/shuntingYard.cpp @@ -41,6 +41,7 @@ std::deque exprToTokens(const std::string & expr) { i += (*p == '{') ? 1 : (*p == '}') ? -1 : 0; ++p; } + const auto s = std::string(b, p); tokens.emplace_back(Token::Type::String, s, -3); if (*p == '\0') { @@ -84,6 +85,7 @@ std::deque exprToTokens(const std::string & expr) { while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p == '_') || (*p & 0x80)) { ++p; } + const auto s = std::string(b, p); tokens.emplace_back(Token::Type::String, s, -3); --p; @@ -93,6 +95,7 @@ std::deque exprToTokens(const std::string & expr) { while (*p && *p != '"') { ++p; } + const auto s = std::string(b, p); tokens.emplace_back(Token::Type::String, s, -3); if (*p == '\0') { @@ -104,6 +107,7 @@ std::deque exprToTokens(const std::string & expr) { while (*p && *p != '\'') { ++p; } + const auto s = std::string(b, p); tokens.emplace_back(Token::Type::String, s, -3); if (*p == '\0') { @@ -114,6 +118,7 @@ std::deque exprToTokens(const std::string & expr) { while (isdigit(*p) || *p == '.') { ++p; } + const auto s = std::string(b, p); tokens.emplace_back(Token::Type::Number, s, -2); --p; @@ -160,6 +165,7 @@ std::deque exprToTokens(const std::string & expr) { tokens.pop_back(); continue; } + token = Token::Type::Unary; c = 'm'; precedence = 5; @@ -167,6 +173,7 @@ std::deque exprToTokens(const std::string & expr) { // otherwise, it's binary '-' precedence = 2; } + break; case '&': if (p[1] == '&') @@ -189,12 +196,14 @@ std::deque exprToTokens(const std::string & expr) { precedence = 2; token = Token::Type::Unary; } + break; case '<': if (p[1] == '=') { ++p; c = '{'; } + precedence = 1; token = Token::Type::Compare; break; @@ -203,6 +212,7 @@ std::deque exprToTokens(const std::string & expr) { ++p; c = '}'; } + precedence = 1; token = Token::Type::Compare; break; @@ -213,6 +223,7 @@ std::deque exprToTokens(const std::string & expr) { token = Token::Type::Compare; break; } + const auto s = std::string(1, c); tokens.emplace_back(token, s, precedence, rightAssociative); } @@ -321,6 +332,7 @@ std::deque shuntingYard(const std::deque & tokens) { queue.push_back(std::move(stack.back())); stack.pop_back(); } + return queue; } @@ -329,9 +341,11 @@ bool isnum(const std::string & s) { if (s.empty() || s.find_first_of("0123456789") == std::string::npos) { return false; } + if (s.find_first_not_of("0123456789.") == std::string::npos || (s[0] == '-' && s.find_first_not_of("0123456789.", 1) == std::string::npos)) { return true; } + return false; } @@ -350,17 +364,20 @@ std::string commands(std::string & expr, bool quotes) { if (e == std::string::npos) { e = expr.length(); } + char cmd[COMMAND_MAX_LENGTH]; size_t l = e - f; if (l >= sizeof(cmd) - 1) { break; } + expr_new.copy(cmd, l, f); cmd[l] = '\0'; if (strstr(cmd, "/value") == nullptr) { strlcat(cmd, "/value", sizeof(cmd) - 6); } + JsonDocument doc_out; JsonDocument doc_in; JsonObject output = doc_out.to(); @@ -373,6 +390,7 @@ std::string commands(std::string & expr, bool quotes) { if (return_code != CommandRet::OK && return_code != CommandRet::NO_VALUE) { return expr = ""; } + std::string data; if (output["api_data"].is()) { data = output["api_data"].as(); @@ -383,12 +401,14 @@ std::string commands(std::string & expr, bool quotes) { } else { serializeJson(output, data); } + expr.replace(f, l, data); e = f + data.length(); expr_new = Helpers::toLower(expr); f = expr_new.find(d, e); } } + if (quotes) { // remove double quotes auto f = expr.find("\"\""); @@ -397,6 +417,7 @@ std::string commands(std::string & expr, bool quotes) { f = expr.find("\"\""); } } + return expr; } @@ -406,6 +427,7 @@ int to_logic(const std::string & s) { if (!Helpers::value2bool(s.c_str(), value_b)) { return -1; // invalid } + return value_b; // if (s.empty()) { @@ -427,9 +449,11 @@ std::string to_string(double d) { while (!s.empty() && s.back() == '0') { s.pop_back(); } + if (!s.empty() && s.back() == '.') { s.pop_back(); } + return s; } @@ -476,6 +500,7 @@ std::string calculate(const std::string & expr) { if (stack.empty()) { return ""; } + const auto rhs = stack.back(); stack.pop_back(); if (token.str[0] == '!') { @@ -489,20 +514,25 @@ std::string calculate(const std::string & expr) { EMSESP::logger().warning("'!' needs a boolean or numeric operand, got '%s'. Literal text must be quoted", rhs.c_str()); return ""; } + break; } + if (token.str[0] == 'h') { // hex string to number if (rhs.empty() || rhs.find_first_not_of("0123456789abcdefABCDEF") != std::string::npos) { return ""; } + stack.push_back(to_string(std::stoi(rhs, 0, 16))); break; } + // bail out on non-numeric operand if (!isnum(rhs)) { return ""; } + auto rhd = std::stod(rhs); switch (token.str[0]) { default: @@ -551,6 +581,7 @@ std::string calculate(const std::string & expr) { if (stack.size() < 2) { return ""; } + const auto rhs = stack.back(); stack.pop_back(); const auto lhs = stack.back(); @@ -564,6 +595,7 @@ std::string calculate(const std::string & expr) { stack.push_back((std::stod(lhs) < std::stod(rhs)) ? "1" : "0"); break; } + stack.push_back((lhs < rhs) ? "1" : "0"); break; case '{': @@ -571,6 +603,7 @@ std::string calculate(const std::string & expr) { stack.push_back((std::stod(lhs) <= std::stod(rhs)) ? "1" : "0"); break; } + stack.push_back((lhs <= rhs) ? "1" : "0"); break; case '>': @@ -578,6 +611,7 @@ std::string calculate(const std::string & expr) { stack.push_back((std::stod(lhs) > std::stod(rhs)) ? "1" : "0"); break; } + stack.push_back((lhs > rhs) ? "1" : "0"); break; case '}': @@ -585,6 +619,7 @@ std::string calculate(const std::string & expr) { stack.push_back((std::stod(lhs) >= std::stod(rhs)) ? "1" : "0"); break; } + stack.push_back((lhs >= rhs) ? "1" : "0"); break; case '=': @@ -592,6 +627,7 @@ std::string calculate(const std::string & expr) { stack.push_back((std::stod(lhs) == std::stod(rhs)) ? "1" : "0"); break; } + // compare strings lower case stack.push_back((Helpers::toLower(lhs) == Helpers::toLower(rhs)) ? "1" : "0"); break; @@ -600,6 +636,7 @@ std::string calculate(const std::string & expr) { stack.push_back((std::stod(lhs) != std::stod(rhs)) ? "1" : "0"); break; } + // compare strings lower case stack.push_back((Helpers::toLower(lhs) != Helpers::toLower(rhs)) ? "1" : "0"); break; @@ -610,6 +647,7 @@ std::string calculate(const std::string & expr) { if (stack.size() < 2) { return ""; } + const auto rhs = to_logic(stack.back()); stack.pop_back(); const auto lhs = to_logic(stack.back()); @@ -617,6 +655,7 @@ std::string calculate(const std::string & expr) { if (rhs < 0 || lhs < 0) { return ""; } + switch (token.str[0]) { default: return ""; @@ -634,6 +673,7 @@ std::string calculate(const std::string & expr) { if (stack.size() < 2) { return ""; } + const auto rhs = stack.back(); stack.pop_back(); const auto lhs = stack.back(); @@ -642,10 +682,12 @@ std::string calculate(const std::string & expr) { stack.push_back(lhs + rhs); break; } + // bail out on non-numeric operands if (!isnum(lhs) || !isnum(rhs)) { return ""; } + auto lhd = std::stod(lhs); auto rhd = std::stod(rhs); switch (token.str[0]) { @@ -686,6 +728,7 @@ std::string calculate(const std::string & expr) { for (const auto & s : stack) { result += s; } + return result; } @@ -704,6 +747,7 @@ static size_t find_unquoted(const std::string & s, char c, size_t from = 0) { return i; } } + return std::string::npos; } @@ -725,6 +769,7 @@ std::string compute(const std::string & expr) { i++; } } + std::string cmd = expr_new.substr(f, e - f).c_str(); JsonDocument doc; if (DeserializationError::Ok == deserializeJson(doc, cmd)) { @@ -747,6 +792,7 @@ std::string compute(const std::string & expr) { keys_s = p.key().c_str(); } } + std::string value = doc[value_s] | ""; std::string method = doc[method_s] | "GET"; std::string result; @@ -758,6 +804,7 @@ std::string compute(const std::string & expr) { if (doc[keys_s].is()) { keys_doc.set(doc[keys_s].as()); } + JsonArray keys = keys_doc.as(); if (key.length() || !keys.isNull()) { doc.clear(); @@ -775,17 +822,21 @@ std::string compute(const std::string & expr) { break; // type mismatch } } + result = json.as(); } + expr_new.replace(f, e - f, result.c_str()); } } + expr_new.replace(f, e - f, result); } else if (httpResult != 0) { // httpResult of 0 means no url EMSESP::logger().warning("URL command failed with https code: %d, response: %s", httpResult, result.c_str()); } } + f = expr_new.find_first_of('{', e); } @@ -799,15 +850,18 @@ std::string compute(const std::string & expr) { q1 = find_unquoted(expr_new, '?', q1 + 1); c1 = find_unquoted(expr_new, ':', c1 + 1); } + if (c1 == std::string::npos) { return ""; // error: missing colon } + auto s = q; // start search brackets int br = 0; // count brackets while (s > 0 && (br || expr_new[s - 1] != '(')) { s--; br += (expr_new[s] == '(' ? -1 : expr_new[s] == ')' ? 1 : 0); } + std::string cond = calculate(expr_new.substr(s, q - s)); if (cond.length() == 0) { return ""; @@ -821,6 +875,7 @@ std::string compute(const std::string & expr) { br += (expr_new[e] == ')' ? -1 : expr_new[e] == '(' ? 1 : 0); } } + expr_new.erase(c1, e + 1 - c1); // remove second expression after colon expr_new.erase(s, q + 1 - s); // remove condition before questionmark } else if (cond[0] == '0') { @@ -828,6 +883,7 @@ std::string compute(const std::string & expr) { } else { return ""; // error } + q = find_unquoted(expr_new, '?'); // search next instance } diff --git a/src/core/system.cpp b/src/core/system.cpp index 39f156a2a..eef75e5af 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -122,9 +122,11 @@ static void show_addresses(uuid::console::Shell & shell, NetworkInterface & neti for (const auto & dns : Network::dns_servers(netif)) { shell.printfln(" %s DNS Server: %s", dns.type() == IPv4 ? "IPv4" : "IPv6", uuid::printable_to_string(dns).c_str()); } + for (const auto & ipv6 : Network::ipv6_addresses(netif)) { shell.printfln(" IPv6 address: %s (%s)", uuid::printable_to_string(ipv6.ip).c_str(), ipv6.scope); } + for (const auto & gateway : Network::ipv6_gateways(netif)) { shell.printfln(" IPv6 gateway: %s", uuid::printable_to_string(gateway).c_str()); } @@ -170,6 +172,7 @@ uint8_t System::language_index() { return i; } } + return 0; // EN only } @@ -198,6 +201,7 @@ bool System::command_sendmail(const char * value, const int8_t) { if (!enabled) { return false; } + LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, security == EMAIL_SECURITY::SSL ? " (SSL)" @@ -219,6 +223,7 @@ bool System::command_sendmail(const char * value, const int8_t) { body = doc["body"] | body; } } + // keep the original body if the calculator returns nothing, so unquoted literal text still gets sent std::string computed_body = compute(body.c_str()); if (!computed_body.empty()) { @@ -269,6 +274,7 @@ bool System::command_sendmail(const char * value, const int8_t) { cleanup(); return false; } + if (!smtp->isConnected()) { LOG_ERROR("send email connection error: %s", smtp->status().text.c_str()); cleanup(); @@ -284,6 +290,7 @@ bool System::command_sendmail(const char * value, const int8_t) { return false; } } + SMTPMessage & msg = smtp->getMessage(); msg.headers.add(rfc822_subject, subject); msg.headers.add(rfc822_from, sender); @@ -324,6 +331,7 @@ std::string System::languages_string() { languages_string += ","; } } + languages_string += ")"; return languages_string; } @@ -338,6 +346,7 @@ bool System::command_response(const char * value, const int8_t id, JsonObject ou } else { output["response"] = Mqtt::get_response(); } + return true; } @@ -454,6 +463,7 @@ bool System::command_watch(const char * value, const int8_t id) { if (w == 0 || EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { EMSESP::watch_id(0); } + if (Mqtt::publish_single() && w != EMSESP::watch()) { if (Mqtt::publish_single2cmd()) { Mqtt::queue_publish("system/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); @@ -461,6 +471,7 @@ bool System::command_watch(const char * value, const int8_t id) { Mqtt::queue_publish("system_data/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); } } + EMSESP::watch(w); return true; } else if (i) { @@ -471,12 +482,15 @@ bool System::command_watch(const char * value, const int8_t id) { Mqtt::queue_publish("system_data/watch", Helpers::hextoa(i)); } } + EMSESP::watch_id(i); if (EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { EMSESP::watch(EMSESP::Watch::WATCH_ON); } + return true; } + return false; } @@ -484,6 +498,7 @@ void System::store_nvs_values() { if (Command::find_command(EMSdevice::DeviceType::BOILER, 0, "nompower", 0) != nullptr) { Command::call(EMSdevice::DeviceType::BOILER, "nompower", "-1"); // trigger a write } + EMSESP::analogsensor_.store_counters(); EMSESP::nvs_.end(); } @@ -543,6 +558,7 @@ void System::get_partition_info() { if (EMSESP::nvs_.isKey(part->label)) { p_info.version = EMSESP::nvs_.getString(part->label).c_str(); } + char c[20]; snprintf(c, sizeof(c), "d_%s", (const char *)part->label); time_t d = EMSESP::nvs_.getULong(c, 0); @@ -556,12 +572,14 @@ void System::get_partition_info() { } else { p_info.size = 0; } + partition_info_[part->label] = p_info; } } it = esp_partition_next(it); // loop to next partition } + esp_partition_iterator_release(it); #endif } @@ -577,14 +595,17 @@ void System::recovery_installed() { if (EMSESP::nvs_.isKey(factory->label)) { EMSESP::nvs_.remove(factory->label); } + const time_t now = time(nullptr); if (now > 1500000000L) { EMSESP::nvs_.putULong(c, now); // UTC epoch, read by the recovery app. Only if the clock is set (NTP) } else if (EMSESP::nvs_.isKey(c)) { EMSESP::nvs_.remove(c); // clock not set, the date of the previous firmware is wrong } + partition_info_.erase(factory->label); // on the AsyncTCP task, same as the web handlers reading it } + LOG_INFO("Recovery firmware v%s installed", recovery_version().c_str()); #endif } @@ -697,6 +718,7 @@ void System::system_restart(const char * partitionname) { LOG_ERROR("Partition '%s' not found", partitionname); return; } + if (strcmp(partition->label, partitionname) && strcmp(partitionname, "boot") != 0) { partition = esp_ota_get_next_update_partition(partition); if (!partition || strcmp(partition->label, partitionname)) { @@ -704,6 +726,7 @@ void System::system_restart(const char * partitionname) { return; } } + // error if partition is empty uint64_t buffer; esp_partition_read(partition, 0, &buffer, 8); @@ -711,6 +734,7 @@ void System::system_restart(const char * partitionname) { LOG_ERROR("Partition '%s' is empty, not bootable", partition->label); return; } + // set the boot partition esp_ota_set_boot_partition(partition); } @@ -728,6 +752,7 @@ void System::system_restart(const char * partitionname) { Shell::loop_all(); delay(10); // give telnet TCP stack time to transmit } + Serial.flush(); // wait for hardware TX buffer to drain Mqtt::disconnect(); // gracefully disconnect MQTT (flushes the DISCONNECT before reboot, needed for QOS1) @@ -767,6 +792,7 @@ void System::syslog_init() { syslog_.mark_interval(0); // syslog_.destination(""); } + if (Mqtt::publish_single()) { if (Mqtt::publish_single2cmd()) { Mqtt::queue_publish("system/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); @@ -806,6 +832,7 @@ void System::modbus_init() { delete EMSESP::modbus_; EMSESP::modbus_ = nullptr; } + modbus_enabled_ = settings.modbus_enabled; modbus_port_ = settings.modbus_port; modbus_max_clients_ = settings.modbus_max_clients; @@ -871,6 +898,7 @@ void System::check_crash_loop() { if (factory == nullptr || factory == esp_ota_get_running_partition()) { return; } + // fails if the factory partition doesn't hold a valid image, then keep trying the current one if (esp_ota_set_boot_partition(factory) == ESP_OK) { crash_count_ = 0; @@ -889,6 +917,7 @@ void System::check_firmware_health() { if (firmware_healthy_ || uuid::get_uptime_sec() < FIRMWARE_HEALTHY_UPTIME) { return; } + firmware_healthy_ = true; crash_count_ = 0; if (OtaUpdater::confirmRunningApp()) { @@ -905,6 +934,7 @@ void System::start() { if (crash_count_) { LOG_WARNING("Restarted after a crash (%u in a row)", crash_count_); } + const esp_partition_t * invalid = esp_ota_get_last_invalid_partition(); if (invalid != nullptr) { LOG_WARNING("Firmware in partition %s failed to start and was rolled back", invalid->label); @@ -986,6 +1016,7 @@ void System::button_OnLongPress(PButton & b) { LOG_NOTICE("reset disabled, can't boot"); return; } + LOG_NOTICE("Button pressed - long press - restart EMS-ESP"); EMSESP::system_.system_restart(); } @@ -997,6 +1028,7 @@ void System::button_OnVLongPress(PButton & b) { LOG_NOTICE("reset disabled, can't boot"); return; } + LOG_NOTICE("Button pressed - very long press - perform factory reset in Recovery Mode"); EMSESP::system_.system_restart("boot"); // EMSESP::led_.start_led_fast_flash(5); // Start LED flash timer for 5 seconds @@ -1009,6 +1041,7 @@ void System::button_init() { LOG_WARNING("Multi-functional button not detected"); return; } + LOG_DEBUG("Multi-functional button enabled"); myPButton_.onClick(BUTTON_Debounce, button_OnClick); @@ -1077,6 +1110,7 @@ void System::send_info_mqtt() { if (!Mqtt::connected() || connection == _connection) { return; } + _connection = connection; JsonDocument doc; @@ -1270,6 +1304,7 @@ int8_t System::wifi_quality(int8_t dBm) { if (dBm >= -50) { return 100; } + return 2 * (dBm + 100); } @@ -1358,12 +1393,14 @@ void System::show_system(uuid::console::Shell & shell) { } else { shell.printfln(" PSRAM: not available"); } + // GPIOs shell.println(" GPIOs:"); shell.printf(" allowed:"); for (const auto & gpio : valid_system_gpios_) { shell.printf(" %d", gpio); } + shell.printfln(" [total %d]", valid_system_gpios_.size()); shell.printf(" in use:"); auto sorted_gpios = used_gpios_; @@ -1371,12 +1408,14 @@ void System::show_system(uuid::console::Shell & shell) { for (const auto & gpio : sorted_gpios) { shell.printf(" %d(%s)", gpio.pin, gpio.source.c_str()); } + shell.printfln(" [total %d]", used_gpios_.size()); auto available = available_gpios(); shell.printf(" available:"); for (const auto & gpio : available) { shell.printf(" %d", gpio); } + shell.printfln(" [total %d]", available.size()); // List all partitions and their version info shell.println(" Partitions:"); @@ -1394,6 +1433,7 @@ void System::show_system(uuid::console::Shell & shell) { if (partition.second.version.empty()) { continue; // no version, empty string } + std::string installed; if (partition.second.install_date > 0) { char time_string[25]; @@ -1401,11 +1441,13 @@ void System::show_system(uuid::console::Shell & shell) { strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); installed = std::string(", installed on ") + time_string; } + const bool is_running = strcmp(running_label, partition.first.c_str()) == 0; const bool is_boot = strcmp(boot_label, partition.first.c_str()) == 0; if (is_boot) { boot_listed = true; } + const char * mark = is_running && is_boot ? "** active, boot **" : is_running ? "** active **" : is_boot ? "** boot **" : ""; shell.printfln(" %s: v%s (%d KB%s) %s", partition.first.c_str(), partition.second.version.c_str(), partition.second.size, installed.c_str(), mark); } @@ -1421,12 +1463,14 @@ void System::show_system(uuid::console::Shell & shell) { if (is_boot) { boot_listed = true; } + int size_kb = 0; esp_image_metadata_t meta = {}; esp_partition_pos_t part_pos = {.offset = factory->address, .size = factory->size}; if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { size_kb = static_cast(meta.image_len / 1024); } + const char * mark = is_running && is_boot ? "** active, boot **" : is_running ? "** active **" : is_boot ? "** boot **" : ""; shell.printfln(" %s: Recovery v%s (%d KB) %s", factory->label, desc.version, size_kb, mark); } @@ -1477,11 +1521,14 @@ void System::show_system(uuid::console::Shell & shell) { shell.printfln(" = unknown"); break; } + err = nvs_entry_next(&it); } + if (it != nullptr) { nvs_release_iterator(it); // just in case } + if (err == ESP_OK) { shell.println(); } @@ -1538,6 +1585,7 @@ void System::show_system(uuid::console::Shell & shell) { if (ethernet_status != nullptr) { shell.printfln(" Ethernet Status: %s", ethernet_status); } + if (EMSESP::network_.ethernet_connected()) { shell.printfln(" Ethernet MAC address: %s", ETH.macAddress().c_str()); shell.printfln(" Hostname: %s", ETH.getHostname()); @@ -1616,24 +1664,31 @@ bool System::check_restore() { } } } + // continue processing the rest of the sections saveSettings(EMSESP_SETTINGS_FILE, section); } + if (section_type == "commands") { saveSettings(EMSESP_COMMANDS_FILE, section); } + if (section_type == "schedule") { saveSettings(EMSESP_SCHEDULER_FILE, section); } + if (section_type == "customizations") { saveSettings(EMSESP_CUSTOMIZATION_FILE, section); } + if (section_type == "entities") { saveSettings(EMSESP_CUSTOMENTITY_FILE, section); } + if (section_type == "modules") { saveSettings(EMSESP_MODULES_FILE, section); } + if (section_type == "customSupport") { // it's a custom support, extract json and write to /config/customSupport.json file File customSupportFile = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "w"); @@ -1660,6 +1715,7 @@ bool System::check_restore() { EMSESP::nvs_.putChar(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_U8: if (entry["value"].is()) { @@ -1667,6 +1723,7 @@ bool System::check_restore() { EMSESP::nvs_.putUChar(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_I32: if (entry["value"].is()) { @@ -1674,6 +1731,7 @@ bool System::check_restore() { EMSESP::nvs_.putInt(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_U32: if (entry["value"].is()) { @@ -1681,6 +1739,7 @@ bool System::check_restore() { EMSESP::nvs_.putUInt(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_I64: if (entry["value"].is()) { @@ -1688,6 +1747,7 @@ bool System::check_restore() { EMSESP::nvs_.putLong64(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_U64: if (entry["value"].is()) { @@ -1695,6 +1755,7 @@ bool System::check_restore() { EMSESP::nvs_.putULong64(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_BLOB: // used for double values @@ -1703,6 +1764,7 @@ bool System::check_restore() { EMSESP::nvs_.putDouble(key.c_str(), v); LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v); } + break; case NVS_TYPE_STR: case NVS_TYPE_ANY: @@ -1712,6 +1774,7 @@ bool System::check_restore() { EMSESP::nvs_.putString(key.c_str(), v.c_str()); LOG_DEBUG("Restored NVS value: %s = %s", key.c_str(), v.c_str()); } + break; } } @@ -1824,6 +1887,7 @@ bool System::check_upgrade() { LOG_INFO("Upgrade: setting MQTT Entity ID format to v3.6 format (4)"); return StateUpdateResult::CHANGED; } + return StateUpdateResult::UNCHANGED; }); } @@ -1840,11 +1904,13 @@ bool System::check_upgrade() { LOG_INFO("Upgrade: setting WiFi TX Power to Auto"); changed = StateUpdateResult::CHANGED; } + if (networkSettings.nosleep != true) { networkSettings.nosleep = true; LOG_INFO("Upgrade: disabling WiFi nosleep"); changed = StateUpdateResult::CHANGED; } + return changed; }); EMSESP::network_.reconnect(); @@ -1873,6 +1939,7 @@ bool System::check_upgrade() { LOG_INFO("Upgrade: setting AP provision mode to on disconnect"); return StateUpdateResult::CHANGED; } + return StateUpdateResult::UNCHANGED; }); // Scheduler name is now mandatory, update FS if name is empty @@ -1885,6 +1952,7 @@ bool System::check_upgrade() { schedule_changed = true; } } + return schedule_changed ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED; }); #endif @@ -1923,9 +1991,11 @@ bool System::check_upgrade() { } else { snprintf(ci.name, sizeof(ci.name), "schedule_%d", idx); } + commands.commandItems.push_back(ci); idx++; } + return StateUpdateResult::CHANGED; }); @@ -1937,12 +2007,14 @@ bool System::check_upgrade() { if (it == scheduler.scheduleItems.end()) { break; } + // flag 132 (0x84) is the old IMMEDIATE format which has no command - erase the entry if (item["flags"].as() == 0x84) { it = scheduler.scheduleItems.erase(it); idx++; continue; } + const char * nm = item["name"]; char cmd_name[sizeof(it->name)]; if (nm != nullptr && nm[0] != '\0') { @@ -1951,10 +2023,12 @@ bool System::check_upgrade() { snprintf(cmd_name, sizeof(cmd_name), "schedule_%d", idx); strlcpy(it->name, cmd_name, sizeof(it->name)); // keep entry name consistent with its command } + it->cmd_name = cmd_name; ++it; idx++; } + return StateUpdateResult::CHANGED; }); @@ -1975,11 +2049,13 @@ bool System::check_upgrade() { return StateUpdateResult::CHANGED; } } + // Migrate language from cz to cs if (settings.locale == "cz") { settings.locale = "cs"; return StateUpdateResult::CHANGED; } + return StateUpdateResult::UNCHANGED; }); } else if (this_version < settings_version) { @@ -2060,8 +2136,10 @@ void System::exportSettings(const std::string & type, [[maybe_unused]] const cha LOG_ERROR("Failed to deserialize settings file %s", filename); return; } + output[section].set(jsonDocument.as()); } + LOG_DEBUG("Exported %s settings from file %s", section, filename); } else { LOG_ERROR("No settings file for %s found", filename); @@ -2177,6 +2255,7 @@ void System::exportSystemBackup(JsonObject output) { entry["value"] = EMSESP::nvs_.getString(info.key); break; } + err = nvs_entry_next(&it); } @@ -2279,6 +2358,7 @@ bool System::command_service(const char * cmd, const char * value) { if (ok) { LOG_INFO("System command '%s' with value '%s'", cmd, value); } + return ok; } @@ -2288,9 +2368,11 @@ bool System::get_value_info(JsonObject output, const char * cmd) { LOG_ERROR("empty system command"); return false; } + if (!strcmp(cmd, "restart")) { // restart is a command, not an entity return false; } + // check for hardcoded "info"/"value" if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) { return command_info("", 0, output); @@ -2303,6 +2385,7 @@ bool System::get_value_info(JsonObject output, const char * cmd) { output["api_data"] = metrics; return true; } + return false; } @@ -2323,6 +2406,7 @@ bool System::get_value_info(JsonObject output, const char * cmd) { } } } + return true; } @@ -2344,9 +2428,11 @@ bool System::get_value_info(JsonObject output, const char * cmd) { for (JsonPair p1 : p.value().as()) { output[p1.key().c_str()] = p1.value().as(); } + return true; } } + return false; } @@ -2363,6 +2449,7 @@ bool System::get_value_info(JsonObject output, const char * cmd) { output["api_data"] = p1.value().as(); return true; } + get_value_json(output, p_key, p1_key, p1.value()); return true; } @@ -2370,6 +2457,7 @@ bool System::get_value_info(JsonObject output, const char * cmd) { } // else skip, but we don't have value pairs in system root } } + return false; } @@ -2378,6 +2466,7 @@ void System::get_value_json(JsonObject output, const std::string & circuit, cons if (circuit.length()) { output["circuit"] = circuit; } + output["readable"] = true; output["writeable"] = (name == "txpause" || name == "showerTimer" || name == "showerAlert" || name == "enabled" || name == "hideLed" || name == "analogEnabled"); @@ -2420,6 +2509,7 @@ std::string System::get_metrics_prometheus() { escaped += c; } } + return escaped; }; @@ -2435,6 +2525,7 @@ std::string System::get_metrics_prometheus() { c = '_'; } } + return sanitized; }; @@ -2446,6 +2537,7 @@ std::string System::get_metrics_prometheus() { c = tolower(c); } } + return result; }; @@ -2454,14 +2546,17 @@ std::string System::get_metrics_prometheus() { if (path == "system" && key == "uptime") { return true; } + if (path == "ntp" && key == "timestamp") { return true; } + if (path.find("devices[") != std::string::npos) { if (key == "handlersReceived" || key == "handlersFetched" || key == "handlersPending" || key == "handlersIgnored") { return true; } } + return false; }; @@ -2519,11 +2614,14 @@ std::string System::get_metrics_prometheus() { if (!first) { result += ", "; } + result += label.first + "=\"" + escape_label(label.second) + "\""; first = false; } + result += "}"; } + result += " " + std::to_string(device["productID"].as()) + "\n"; } @@ -2544,11 +2642,14 @@ std::string System::get_metrics_prometheus() { if (!first) { result += ", "; } + result += label.first + "=\"" + escape_label(label.second) + "\""; first = false; } + result += "}"; } + result += " " + std::to_string(device["entities"].as()) + "\n"; } } @@ -2578,6 +2679,7 @@ std::string System::get_metrics_prometheus() { snprintf(val_str, sizeof(val_str), "%.2f", p.value().as()); result += val_str; } + result += "\n"; } else if (is_string) { // collect string for info metric (skip dynamic strings like uptime and timestamp) @@ -2592,6 +2694,7 @@ std::string System::get_metrics_prometheus() { break; } } + if (!key_exists) { local_info_labels.push_back({lower_key, val}); } @@ -2617,11 +2720,14 @@ std::string System::get_metrics_prometheus() { if (!first) { result += ", "; } + result += label.first + "=\"" + escape_label(label.second) + "\""; first = false; } + result += "}"; } + result += " 1\n"; } }; @@ -2692,6 +2798,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["psramSize"] = EMSESP::system_.PSram(); node["freePsram"] = ESP.getFreePsram() / 1024; } + node["model"] = EMSESP::system_.getBBQKeesGatewayDetails(); #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 node["temperature"] = EMSESP::system_.temperature(); @@ -2705,8 +2812,10 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output if (!gpios_allowed_str.empty()) { gpios_allowed_str += ", "; } + gpios_allowed_str += Helpers::itoa(gpio); } + node["gpios_allowed"] = gpios_allowed_str; std::string gpios_in_use_str; @@ -2716,8 +2825,10 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output if (!gpios_in_use_str.empty()) { gpios_in_use_str += ", "; } + gpios_in_use_str += Helpers::itoa(gpio.pin); } + node["gpios_in_use"] = gpios_in_use_str; std::string gpios_available_str; @@ -2725,8 +2836,10 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output if (!gpios_available_str.empty()) { gpios_available_str += ", "; } + gpios_available_str += Helpers::itoa(gpio); } + node["gpios_available"] = gpios_available_str; // Network Status @@ -2765,6 +2878,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output if (WiFi.status() == WL_CONNECTED && !settings.bssid.isEmpty()) { node["BSSID"] = "set"; // we don't disclose the name } + node["TxPowerSetting"] = settings.tx_power; node["staticIP"] = settings.staticIPConfig; node["lowBandwidth"] = settings.bandwidth20; @@ -2819,6 +2933,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["MQTTPublishFails"] = Mqtt::publish_fails(); node["MQTTReconnects"] = Mqtt::connect_count(); } + EMSESP::esp32React.getMqttSettingsService()->read([&](const MqttSettings & settings) { node["enabled"] = settings.enabled; node["clientID"] = settings.clientId; @@ -2911,6 +3026,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["busStatus"] = "unknown"; break; } + node["busProtocol"] = EMSbus::is_ht3() ? "HT3" : "Buderus"; node["busTelegramsReceived"] = EMSESP::rxservice_.telegram_count(); node["busReads"] = EMSESP::txservice_.telegram_read_count(); @@ -2935,6 +3051,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["showerAlertColdshot"] = settings.shower_alert_coldshot; // seconds node["showerAlertTrigger"] = settings.shower_alert_trigger; // minutes } + if (settings.board_profile == "CUSTOM") { node["phyType"] = settings.phy_type; if (settings.phy_type != PHY_type::PHY_TYPE_NONE) { @@ -2942,6 +3059,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["ethPhyAddr"] = settings.eth_phy_addr; node["ethClockMmode"] = settings.eth_clock_mode; } + node["rxGPIO"] = settings.rx_gpio; node["txGPIO"] = settings.tx_gpio; node["dallasGPIO"] = settings.dallas_gpio; @@ -2949,6 +3067,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["ledGPIO"] = settings.led_gpio; node["ledType"] = settings.led_type; } + node["hideLed"] = settings.hide_led; node["noTokenApi"] = settings.notoken_api; node["readonlyMode"] = settings.readonly_mode; @@ -2994,14 +3113,17 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output if (result[0] != '\0') { obj["handlersReceived"] = result; // don't show handlers if there aren't any } + (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::FETCHED); if (result[0] != '\0') { obj["handlersFetched"] = result; } + (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::PENDING); if (result[0] != '\0') { obj["handlersPending"] = result; } + (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::IGNORED); if (result[0] != '\0') { obj["handlersIgnored"] = result; @@ -3018,24 +3140,28 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output obj["name"] = F_(temperaturesensor); obj["entities"] = EMSESP::temperaturesensor_.count_entities(); } + if (EMSESP::analogsensor_.count_entities()) { JsonObject obj = devices.add(); obj["type"] = F_(analogsensor); obj["name"] = F_(analogsensor); obj["entities"] = EMSESP::analogsensor_.count_entities(); } + if (EMSESP::webSchedulerService.count_entities()) { JsonObject obj = devices.add(); obj["type"] = F_(scheduler); obj["name"] = F_(scheduler); obj["entities"] = EMSESP::webSchedulerService.count_entities(); } + if (EMSESP::webCommandService.count_entities()) { JsonObject obj = devices.add(); obj["type"] = F_(commands); obj["name"] = F_(commands); obj["entities"] = EMSESP::webCommandService.count_entities(); } + if (EMSESP::webCustomEntityService.count_entities()) { JsonObject obj = devices.add(); obj["type"] = F_(custom); @@ -3200,6 +3326,7 @@ bool System::command_txpause(const char * value, const int8_t) { #endif } } + return true; } @@ -3331,6 +3458,7 @@ String System::getBBQKeesGatewayDetails([[maybe_unused]] uint8_t detail) { uint32_t model : 4; uint32_t mfg : 4; }; + uint32_t reg; } gw; @@ -3422,6 +3550,7 @@ bool System::uploadFirmwareURL(const char * url) { ssl_client.setBufferSizes(HttpClient::TLS_RX_BUFFER_SIZE, HttpClient::TLS_TX_BUFFER_SIZE); ssl_client.setSessionTimeout(HttpClient::TLS_SESSION_TIMEOUT_S); } + basic_client.setTimeout(FIRMWARE_UPLOAD_READ_TIMEOUT_S * 1000); // socket-level read timeout, in ms ssl_client.setTimeout(FIRMWARE_UPLOAD_READ_TIMEOUT_S); // Stream::readBytes timeout used by Update ssl_client.setClient(&basic_client, is_https); // enableSSL = false for plain HTTP @@ -3479,10 +3608,12 @@ bool System::uploadFirmwareURL(const char * url) { if (line.isEmpty()) { break; // end of headers } + int colon = line.indexOf(':'); if (colon < 0) { continue; } + String name = line.substring(0, colon); name.toLowerCase(); String val = line.substring(colon + 1); @@ -3501,6 +3632,7 @@ bool System::uploadFirmwareURL(const char * url) { LOG_ERROR("Firmware upload failed - too many redirects"); return false; } + String lower_loc = location; lower_loc.toLowerCase(); if (lower_loc.startsWith("https://") || lower_loc.startsWith("http://")) { @@ -3511,6 +3643,7 @@ bool System::uploadFirmwareURL(const char * url) { LOG_ERROR("Firmware upload failed - cross-scheme redirect to %s", location.c_str()); return false; } + url_remain = location.substring(new_is_https ? 8 : 7); } else if (location.startsWith("/")) { url_remain = host + location; // relative redirect, same host @@ -3518,6 +3651,7 @@ bool System::uploadFirmwareURL(const char * url) { LOG_ERROR("Firmware upload failed - unsupported redirect to %s", location.c_str()); return false; } + LOG_DEBUG("Following redirect to %s", url_remain.c_str()); continue; } @@ -3540,6 +3674,7 @@ bool System::uploadFirmwareURL(const char * url) { while (ssl_client.connected() && !ssl_client.available() && millis() - body_wait < FIRMWARE_UPLOAD_RESPONSE_TIMEOUT) { delay(1); } + if (!ssl_client.available()) { ssl_client.stop(); LOG_ERROR("Firmware upload failed - no body received"); @@ -3577,6 +3712,7 @@ bool System::uploadFirmwareURL(const char * url) { LOG_ERROR("The recovery firmware can't be installed on this board"); return false; // error } + if (!ota.beginFirmware(firmware_size, factory, false)) { LOG_ERROR("Recovery firmware upload failed - %s", ota.errorString()); return false; // error @@ -3611,6 +3747,7 @@ bool System::uploadFirmwareURL(const char * url) { } else { Update.abort(); // release the OTA partition handle so a later attempt can start cleanly } + ssl_client.stop(); // drop the connection saved_url.clear(); // prevent it from downloading again EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL); @@ -3624,13 +3761,16 @@ bool System::uploadFirmwareURL(const char * url) { if (!ssl_client.connected()) { break; } + if (millis() - wait_start > FIRMWARE_UPLOAD_STALL_TIMEOUT) { break; } + // also bail out promptly if a cancel arrives mid-stall if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) { break; } + delay(1); } @@ -3639,10 +3779,12 @@ bool System::uploadFirmwareURL(const char * url) { if (EMSESP::system_.systemStatus() < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) { continue; } + LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size); if (is_recovery) { ota.abort(); } + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD); return false; } @@ -3658,6 +3800,7 @@ bool System::uploadFirmwareURL(const char * url) { if (is_recovery) { ota.abort(); } + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD); return false; } @@ -3670,18 +3813,22 @@ bool System::uploadFirmwareURL(const char * url) { if (is_recovery) { ota.abort(); } + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD); return false; } + // the recovery firmware must be for this chip, and really be the recovery if (is_recovery && !(OtaUpdater::isCompatibleFirmware(buf, n) && OtaUpdater::isRecoveryFirmware(buf, n))) { LOG_ERROR("Recovery firmware upload failed - not a recovery firmware image for this chip"); if (is_recovery) { ota.abort(); } + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD); return false; } + magic_ok = true; } @@ -3694,6 +3841,7 @@ bool System::uploadFirmwareURL(const char * url) { if (is_recovery) { ota.abort(); } + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD); return false; } @@ -3775,6 +3923,7 @@ bool System::readCommand(const char * data) { } else if (num_args == 2) { length = Helpers::atoint(value); // decimal } + num_args++; } } @@ -4063,6 +4212,7 @@ std::vector System::available_gpios() { gpios.push_back(gpio); // didn't find it in used_gpios_, so it's available } } + return gpios; } @@ -4071,6 +4221,7 @@ void System::make_snapshot_gpios(std::vector & u_gpios, std::vector> valid_system_gpios_; // list of valid GPIOs for the ESP32 board that can be used static std::vector> used_gpios_; // list of GPIOs used by the application diff --git a/src/core/telegram.cpp b/src/core/telegram.cpp index 9d5af1638..af62d79fe 100644 --- a/src/core/telegram.cpp +++ b/src/core/telegram.cpp @@ -56,6 +56,7 @@ uint8_t EMSbus::calculate_crc(const uint8_t * data, const uint8_t length) { crc = ems_crc_table[crc]; crc ^= data[i++]; } + return crc; } @@ -110,6 +111,7 @@ std::string Telegram::to_string() const { data[2] = this->type_id; length = 4; } + for (uint8_t i = 0; i < this->message_length; i++) { data[length++] = this->message_data[i]; } @@ -159,12 +161,14 @@ void RxService::add(uint8_t * data, uint8_t length) { if (!bus_connected()) { return; } + if (data[0] != EMSuart::last_tx_src()) { // do not count echos as errors telegram_error_count_++; LOG_WARNING("Incomplete Rx: %s (crc: %02X)", Helpers::data_to_hex(data, length).c_str(), crc); // include CRC } else { LOG_TRACE("Incomplete Rx: %s (crc: %02X)", Helpers::data_to_hex(data, length).c_str(), crc); // include CRC } + return; } @@ -207,11 +211,13 @@ void RxService::add(uint8_t * data, uint8_t length) { message_data = data + 6; message_length = length - 7; } + // EMS+ boilers broadcast UBAMonitorFastPlus (0xE4); EMS/HT3 use 0x18. // Extended telegrams alone are not enough: Junkers HT3 also uses them (e.g. 0x16F). if (type_id == 0xE4) { set_ems2(); } + // if we're watching and "raw" print out actual telegram as bytes to the console // including the CRC at the end if (EMSESP::watch() == EMSESP::Watch::WATCH_RAW) { @@ -293,8 +299,10 @@ uint8_t TxService::get_send_id() { count = 0; return tx_telegrams_.empty() ? ems_bus_id() : tx_telegrams_.front().telegram_->src; } + return tx_telegrams_.front().telegram_->src; } + count = 0; return ems_bus_id(); } @@ -312,6 +320,7 @@ void TxService::send() { send_poll(); return; } + delayed_send_ = 0; // if we're in read-only mode (tx_mode 0) forget the Tx call @@ -334,6 +343,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { if (ems_mask() != EMS_MASK_UNSET) { src ^= ems_mask(); } + telegram_raw[0] = src; // dest - for READ the MSB must be set @@ -343,6 +353,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { if (telegram->operation == Telegram::Operation::TX_READ) { dest |= 0x80; // read has 8th bit set for the destination } + telegram_raw[1] = dest; uint8_t message_p = 0; // this is the position in the telegram where we want to put our message data @@ -390,6 +401,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { telegram_raw[message_p++] = telegram->message_data[i]; } } + // make a copy of the telegram with new dest (without read-flag) telegram_last_ = std::make_shared( telegram->operation, telegram->src, dest & 0x7F, telegram->type_id, telegram->offset, telegram->message_data, telegram->message_length); @@ -424,6 +436,7 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { } else { increment_telegram_write_fail_count(); // another Tx fail } + tx_state(Telegram::Operation::NONE); // nothing send, tx not in wait state return; } @@ -437,8 +450,10 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) { EMSESP::set_read_id(telegram->type_id); } } + return; } + tx_state(telegram->operation); // tx now in a wait state } @@ -463,6 +478,7 @@ void TxService::add(const uint8_t operation, } else { telegram_read_fail_count_++; } + tx_telegrams_.pop_front(); } #endif @@ -472,6 +488,7 @@ void TxService::add(const uint8_t operation, } else { tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram), false, validateid); // add to back of queue } + if (validateid != 0) { EMSESP::wait_validate(validateid); } @@ -543,6 +560,7 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt } else { telegram_read_fail_count_++; } + tx_telegrams_.pop_front(); } @@ -553,6 +571,7 @@ void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t lengt } else { tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram), false, validate_id); // add to back of queue } + if (validate_id != 0) { EMSESP::wait_validate(validate_id); } @@ -576,6 +595,7 @@ void TxService::read_request(const uint16_t type_id, const uint8_t dest, const u if (length > 0 && length < message_data) { message_data = length; } + add(Telegram::Operation::TX_READ, dest, type_id, offset, &message_data, 1, 0, front); } @@ -597,6 +617,7 @@ bool TxService::send_raw(const char * telegram_data) { data[count++] = (uint8_t)strtol(p, 0, 16); p = strtok(nullptr, " ,"); } + free(telegram); // check valid length if (count < 4) { @@ -621,8 +642,10 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui EMSESP::set_response_id(0); EMSESP::set_read_id(0); } + return; } + increment_telegram_read_fail_count(); // another Tx fail } else { increment_telegram_write_fail_count(); // another Tx fail @@ -636,6 +659,7 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui if (operation == Telegram::Operation::TX_READ) { EMSESP::rxservice_.add_empty(telegram_last_->dest, telegram_last_->src, telegram_last_->type_id, telegram_last_->offset); } + return; } @@ -652,6 +676,7 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui EMSESP::wait_validate(0); // do not wait for validation return; } + // for the last try wait 2 sec before sending. delayed_send_ = (retry_count_ < MAXIMUM_TX_RETRIES) ? 0 : (uuid::get_uptime() + POST_SEND_DELAY); tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram_last_), true, get_post_send_query()); @@ -672,14 +697,17 @@ uint16_t TxService::read_next_tx(const uint8_t offset, const uint8_t length) { if (offset + old_length >= telegram_last_->offset + telegram_last_->message_data[0]) { return 0; } + // we request all and get a short telegram with requested offset if ((next_length + next_offset) == 0xFF && old_length < max_length - 4 && offset <= telegram_last_->offset) { return 0; } + if (offset >= telegram_last_->offset && old_length > 0 && next_length > 0) { add(Telegram::Operation::TX_READ, telegram_last_->dest, telegram_last_->type_id, next_offset, &next_length, 1, 0, true); return telegram_last_->type_id; } + return 0; } @@ -713,4 +741,4 @@ uint16_t TxService::post_send_query() { return post_typeid; } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/core/telegram.h b/src/core/telegram.h index 21da888bd..9fae69105 100644 --- a/src/core/telegram.h +++ b/src/core/telegram.h @@ -210,6 +210,7 @@ class EMSbus { if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) { bus_connected_ = false; } + return bus_connected_; #endif } @@ -220,6 +221,7 @@ class EMSbus { if (!last_bus_activity_) { bus_uptime_start_ = timestamp; } + last_bus_activity_ = timestamp; bus_connected_ = true; } @@ -229,12 +231,14 @@ class EMSbus { if (!bus_uptime_start_) { return 0; // not yet initialized } + return (uint32_t)((uuid::get_uptime() - bus_uptime_start_) / 1000ULL); } static uint8_t tx_state() { return tx_state_; } + static void tx_state(uint8_t tx_state) { tx_state_ = tx_state; } @@ -280,6 +284,7 @@ class RxService : public EMSbus { if (telegram_error_count_ == 0) { return 100; // all good, 100% } + uint8_t q = (telegram_error_count_ * 100 / (telegram_count_ + telegram_error_count_)); return (q <= EMS_BUS_QUALITY_RX_THRESHOLD ? 100 : 100 - q); } @@ -359,6 +364,7 @@ class TxService : public EMSbus { uint32_t telegram_read_count() const { return telegram_read_count_; } + uint32_t telegram_write_count() const { return telegram_write_count_; } @@ -396,6 +402,7 @@ class TxService : public EMSbus { if (telegram_read_fail_count_ == 0) { return 100; // all good, 100% } + return (100 - (uint8_t)(telegram_read_fail_count_ * 100 / (telegram_read_fail_count_ + telegram_read_count_))); } @@ -403,6 +410,7 @@ class TxService : public EMSbus { if (telegram_write_fail_count_ == 0) { return 100; // all good, 100% } + return (100 - (uint8_t)(telegram_write_fail_count_ * 100 / (telegram_write_fail_count_ + telegram_write_count_))); } @@ -464,4 +472,4 @@ class TxService : public EMSbus { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/core/temperaturesensor.cpp b/src/core/temperaturesensor.cpp index d7d124566..5946c8788 100644 --- a/src/core/temperaturesensor.cpp +++ b/src/core/temperaturesensor.cpp @@ -97,6 +97,7 @@ void TemperatureSensor::loop() { } } } + last_activity_ = time_now; } } else if (state_ == State::READING) { @@ -127,6 +128,7 @@ void TemperatureSensor::loop() { if (!parasite_) { bus_.depower(); } + if (bus_.crc8(addr, ADDR_LEN - 1) == addr[ADDR_LEN - 1]) { switch (addr[0]) { case TYPE_DS18B20: @@ -147,11 +149,13 @@ void TemperatureSensor::loop() { publish_sensor(sensor); changed_ |= true; } + sensor.read = true; found = true; break; } } + // add new sensor. this will create the id string, empty name and offset if (!found && (sensors_.size() < (MAX_SENSORS - 1))) { // LOG_NOTICE("Adding new sensor for %s", Sensor(addr).id().c_str()); @@ -169,6 +173,7 @@ void TemperatureSensor::loop() { } else { sensorfails_++; } + break; default: @@ -184,6 +189,7 @@ void TemperatureSensor::loop() { if (!parasite_) { bus_.depower(); } + // check for missing sensors after some samples if (++scancnt_ > SCAN_MAX) { for (auto & sensor : sensors_) { @@ -191,8 +197,10 @@ void TemperatureSensor::loop() { sensor.temperature_c = EMS_VALUE_INT16_NOTSET; changed_ = true; } + sensor.read = false; } + scancnt_ = 0; } else if (scancnt_ == SCAN_START + 1) { // startup firstscan_ = sensors_.size(); @@ -208,11 +216,13 @@ void TemperatureSensor::loop() { } } } + s->set_name("gateway_temperature"); s->set_is_system(true); // mark as internal system temperature sensor if (!EMSESP::nvs_.isKey("intTemp")) { EMSESP::nvs_.putString("intTemp", s->id()); } + LOG_DEBUG("Adding system sensor for gateway_temperature"); EMSESP::webCustomizationService.update([&](WebCustomization & settings) { auto newSensor = SensorCustomization(); @@ -224,11 +234,13 @@ void TemperatureSensor::loop() { return StateUpdateResult::CHANGED; }); } + // LOG_NOTICE("Adding %d sensor(s) from first scan", firstscan_); } else if ((scancnt_ <= 0) && (firstscan_ != sensors_.size())) { // check 2 times for no change of sensor # scancnt_ = SCAN_START; sensors_.clear(); // restart scanning and clear to get correct numbering } + state_ = State::IDLE; } } @@ -241,6 +253,7 @@ bool TemperatureSensor::temperature_convert_complete(const uint32_t time) { if (parasite_) { return time > CONVERSION_MS; // don't care, use the datasheet time } + return bus_.read_bit() == 1; #else return time > CONVERSION_MS; @@ -300,6 +313,7 @@ int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) { break; } } + raw_value = ((int32_t)raw_value * 625 + 500) / 1000; // round to 0.1 return raw_value; #else @@ -338,6 +352,7 @@ bool TemperatureSensor::update(const char * id, const char * name, int16_t offse break; } } + if (!found) { auto newSensor = SensorCustomization(); strlcpy(newSensor.id, id, sizeof(newSensor.id)); @@ -347,6 +362,7 @@ bool TemperatureSensor::update(const char * id, const char * name, int16_t offse settings.sensorCustomizations.push_back(newSensor); LOG_DEBUG("Adding new customization for sensor ID %s", id); } + sensor.ha_registered = false; // it's changed so we may need to recreate the HA config return StateUpdateResult::CHANGED; }); @@ -363,6 +379,7 @@ bool TemperatureSensor::updated_values() { changed_ = false; return true; } + return false; } @@ -380,6 +397,7 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons output[sensor.name()] = serialized(Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0)); } } + return true; } @@ -387,6 +405,7 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons for (const auto & sensor : sensors_) { get_value_json(output[sensor.name()].to(), sensor); } + return true; } @@ -396,6 +415,7 @@ bool TemperatureSensor::get_value_info(JsonObject output, const char * cmd, cons output["api_data"] = metrics; return true; } + return false; } @@ -425,6 +445,7 @@ std::string TemperatureSensor::get_metrics_prometheus() { result += (std::string) "emsesp_" + sensor.name() + " " + Helpers::render_value(val, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0) + "\n"; } + return result; } @@ -455,9 +476,11 @@ void TemperatureSensor::publish_sensor(const Sensor & sensor) { } else { snprintf(topic, sizeof(topic), "%s%s/%s", F_(temperaturesensor), "_data", sensor.name()); } + char payload[10]; Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0)); } + char cmd[COMMAND_MAX_LENGTH]; snprintf(cmd, sizeof(cmd), "%s/%s", F_(temperaturesensor), sensor.name()); EMSESP::webSchedulerService.onChange(cmd); @@ -491,6 +514,7 @@ void TemperatureSensor::publish_values(const bool force) { for (const auto & sensor : sensors_) { publish_sensor(sensor); } + return; } else if (!EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { return; // wait for first time period @@ -609,6 +633,7 @@ const char * TemperatureSensor::Sensor::name() const { if (name_[0] == '\0') { return id_; } + return name_; } @@ -628,6 +653,7 @@ bool TemperatureSensor::Sensor::apply_customization() { } } } + return false; }); @@ -669,4 +695,4 @@ void TemperatureSensor::load_test_data() { } #endif -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/core/temperaturesensor.h b/src/core/temperaturesensor.h index 42fc29be4..c84e50b10 100644 --- a/src/core/temperaturesensor.h +++ b/src/core/temperaturesensor.h @@ -48,6 +48,7 @@ class TemperatureSensor { bool is_system() const { return is_system_; } + void set_is_system(const bool is_system) { is_system_ = is_system; } @@ -59,6 +60,7 @@ class TemperatureSensor { int16_t offset() const { return offset_; } + void set_offset(const int16_t offset) { offset_ = offset; } @@ -124,6 +126,7 @@ class TemperatureSensor { if (exclude_disabled_system) { return std::count_if(sensors_.begin(), sensors_.end(), [](const Sensor & sensor) { return !sensor.is_system(); }); } + return sensors_.size(); } @@ -192,4 +195,4 @@ class TemperatureSensor { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/devices/alert.cpp b/src/devices/alert.cpp index d1556458d..965f6a65d 100644 --- a/src/devices/alert.cpp +++ b/src/devices/alert.cpp @@ -32,9 +32,10 @@ Alert::Alert(uint8_t device_type, uint8_t device_id, uint8_t product_id, const c register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &setFlowTemp_, DeviceValueType::UINT8, FL_(setFlowTemp), DeviceValueUOM::DEGREES); register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &setBurnPow_, DeviceValueType::UINT8, FL_(setBurnPow), DeviceValueUOM::PERCENT); } + // UBASetPoint 0x1A void Alert::process_UBASetPoints(const std::shared_ptr & telegram) { has_update(telegram, setFlowTemp_, 0); // boiler set temp from thermostat has_update(telegram, setBurnPow_, 1); // max burner power in % } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/devices/boiler.cpp b/src/devices/boiler.cpp index a3e87649d..5f9c94850 100644 --- a/src/devices/boiler.cpp +++ b/src/devices/boiler.cpp @@ -282,6 +282,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const 0, 10); } + register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &heatingActivated_, DeviceValueType::BOOL, @@ -753,6 +754,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const DeviceValueUOM::NONE, MAKE_CF_CB(set_maxHeatDhw)); } + register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &auxHeaterSource_, DeviceValueType::ENUM, @@ -1102,6 +1104,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const 120, 240); } + register_device_value(DeviceValueTAG::TAG_DHW1, &wwCircMode_, DeviceValueType::ENUM, @@ -1192,12 +1195,15 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const if (nrgHeatF_ < 0 || nrgHeatF_ >= EMS_VALUE_UINT32_NOTSET) { nrgHeatF_ = 0; } + if (nrgWwF_ < 0 || nrgWwF_ >= EMS_VALUE_UINT32_NOTSET) { nrgWwF_ = 0; } + if (nomPower_ == EMS_VALUE_UINT8_NOTSET) { nomPower_ = 0; } + store_energy(); // update/publish the values has_update(nrgHeat_, (uint32_t)(nrgHeatF_ + 0.5)); @@ -1211,12 +1217,15 @@ void Boiler::store_energy() { if (nrgHeatF_ != EMSESP::nvs_.getDouble("nrgheat")) { EMSESP::nvs_.putDouble("nrgheat", nrgHeatF_); } + if (nrgWwF_ != EMSESP::nvs_.getDouble("nrgww")) { EMSESP::nvs_.putDouble("nrgww", nrgWwF_); } + if (nomPower_ != EMSESP::nvs_.getUChar("nompower")) { EMSESP::nvs_.putUChar("nompower", nomPower_); } + // LOG_DEBUG("energy values stored"); } @@ -1250,6 +1259,7 @@ void Boiler::check_active() { if (Helpers::hasValue(wwCurFlow_) && (wwCurFlow_ > 0) && (wwType_ == 1)) { flowsensor = true; } + if (flowsensor) { b = ((wwCurFlow_ > 0) && ((boilerState_ & 0x0A) == 0x0A)); } else { @@ -1271,6 +1281,7 @@ void Boiler::check_active() { EMSESP::webSettingsService.read([&](WebSettings & settings) { forceHeatingOff_ = settings.boiler_heatingoff ? EMS_VALUE_BOOL_ON : 0; }); has_update(&forceHeatingOff_); } + static uint32_t lastSendHeatingOff = 0; if (forceHeatingOff_ == EMS_VALUE_BOOL_ON && (uuid::get_uptime_sec() - lastSendHeatingOff) >= 60) { lastSendHeatingOff = uuid::get_uptime_sec(); @@ -1278,6 +1289,7 @@ void Boiler::check_active() { uint8_t data[] = {1, 0, 0, 1, 1}; write_command(EMS_TYPE_UBASetPoints2, 0, data, sizeof(data), 0); } + uint8_t data[] = {0, 0, 0, 0}; write_command(EMS_TYPE_UBASetPoints, 0, data, sizeof(data), 0); } @@ -1304,6 +1316,7 @@ void Boiler::check_active() { } else if (curBurnPow_ == 0 && (heatBurnPow + wwBurnPow) > 0) { // on burner switch off store_energy(); } + // store new modulation and time heatBurnPow = heatingActive_ && !tapwaterActive_ ? curBurnPow_ : 0; wwBurnPow = tapwaterActive_ ? curBurnPow_ : 0; @@ -1319,6 +1332,7 @@ void Boiler::process_UBAFactory(const std::shared_ptr & telegram if (telegram->offset > 4 || telegram->offset + telegram->message_length < 7) { return; } + toggle_fetch(telegram->type_id, false); // only read once uint8_t min = 0; uint8_t max = 0; @@ -1330,6 +1344,7 @@ void Boiler::process_UBAFactory(const std::shared_ptr & telegram if (nomPower > 0 && nomPower_ == 0) { has_update(nomPower_, nomPower); } + if (min <= max) { // set_minmax(&burnMinPower_, 0, max); // removed #2918 set_minmax(&burnMaxPower_, min, max); @@ -1389,6 +1404,7 @@ void Boiler::process_UBAMonitorFast(const std::shared_ptr & tele curBurnPow = 100; selBurnPow = 100; } + has_update(selBurnPow_, selBurnPow); has_update(curBurnPow_, curBurnPow); @@ -1464,6 +1480,7 @@ void Boiler::process_UBAParameterWW(const std::shared_ptr & tele } else { wwComfort = EMS_VALUE_UINT8_NOTSET; } + has_update(wwComfort_, wwComfort); } } @@ -1520,16 +1537,18 @@ void Boiler::process_UBAMonitorFastPlus(const std::shared_ptr & if (syspress == 0) { syspress = EMS_VALUE_UINT8_NOTSET; } - has_update(sysPress_, syspress); + has_update(sysPress_, syspress); //has_update(telegram, temperatur_, 13); // unknown temperature has_update(telegram, heatblock_, 23); // see #1317 has_update(telegram, headertemp_, 25); // see #1317 //has_update(telegram, temperatur_, 27); // unknown temperature + telegram->read_value(exhaustTemp1_, 31); if (Helpers::hasValue(exhaustTemp1_)) { has_update(exhaustTemp_, exhaustTemp1_); } + // heating sends a second 0xE4 with 0xFFFF here, which would clear a valid flow, #3224 uint16_t pc0flow = pc0Flow_; if (telegram->read_value(pc0flow, 36) && Helpers::hasValue(pc0flow)) { // see https://github.com/emsesp/EMS-ESP32/issues/2001 @@ -1597,10 +1616,12 @@ void Boiler::process_UBAMonitorSlowPlus2(const std::shared_ptr & if (telegram->read_value(state, 2)) { boilerState_ |= state == 1 ? 0x09 : 0; // heating 0/1 } + state = EMS_VALUE_UINT8_NOTSET; if (telegram->read_value(state, 5)) { boilerState_ |= state == 1 ? 0x0A : 0; // dhw 0/1 } + check_active(); // do a quick check to see if the hot water or heating is active } } @@ -1620,6 +1641,7 @@ void Boiler::process_UBAMonitorSlowPlus(const std::shared_ptr & if (!Helpers::hasValue(exhaustTemp1_)) { has_update(telegram, exhaustTemp_, 6); } + has_update(telegram, burnStarts_, 10, 3); // force to 3 bytes has_update(telegram, burnWorkMin_, 13, 3); // force to 3 bytes has_update(telegram, burn2WorkMin_, 16, 3); // force to 3 bytes @@ -1686,6 +1708,7 @@ void Boiler::process_UBAParameterWWPlus(const std::shared_ptr & } else { wwComfort1 = EMS_VALUE_UINT8_NOTSET; } + has_update(wwComfort1_, wwComfort1); } } @@ -1779,6 +1802,7 @@ void Boiler::process_HpPower(const std::shared_ptr & telegram) { if (coolingType_ == 3) { has_bitupdate(telegram, hp4wayValve_, 0, 3); // https://github.com/emsesp/EMS-ESP32/issues/2844#issuecomment-3869770845 } + has_bitupdate(telegram, elHeatStep1_, 3, 0); has_bitupdate(telegram, elHeatStep2_, 3, 1); has_bitupdate(telegram, elHeatStep3_, 3, 2); @@ -1797,7 +1821,7 @@ void Boiler::process_HpPower(const std::shared_ptr & telegram) { has_bitupdate(telegram, hpInput[2].state, 1, 6); has_bitupdate(telegram, hpInput[3].state, 1, 7); - // from https://github.com/emsesp/EMS-ESP32/issues/2669 + // https://github.com/emsesp/EMS-ESP32/issues/2669 has_update(telegram, hpTargetSpd_, 22); has_update(telegram, receiverValveVr0_, 15); has_update(telegram, expansionValveVr1_, 16); @@ -1860,11 +1884,13 @@ void Boiler::process_HpInConfig(const std::shared_ptr & telegram telegram->read_value(option[j], index[j] + i); option[j] = option[j] == 1 ? '1' : '0'; } + option[12] = atoi(&hpInput[i].option[12]); telegram->read_value(option[12], 27 + i); // modulation Helpers::smallitoa(&option[12], (uint16_t)option[12]); has_update(hpInput[i].option, option, 16); } + // input 4 [] uint8_t index4[] = {42, 43, 44, 45, 46, 47, 52, 50, 49, 48}; for (uint8_t j = 0; j < 9; j++) { @@ -1872,6 +1898,7 @@ void Boiler::process_HpInConfig(const std::shared_ptr & telegram telegram->read_value(option[j], index4[j]); option[j] = option[j] == 1 ? '1' : '0'; } + option[9] = atoi(&hpInput[3].option[9]); telegram->read_value(option[9], 48); // modulation Helpers::smallitoa(&option[9], (uint16_t)option[9]); @@ -1893,6 +1920,7 @@ void Boiler::process_HpHeaterConfig(const std::shared_ptr & tele has_enumupdate(telegram, maxHeatDhw_, 4, {0, 2, 4, 5}); return; } + has_update(telegram, maxHeatComp_, 2); has_update(telegram, maxHeatHeat_, 3); has_update(telegram, maxHeatDhw_, 4); @@ -1973,6 +2001,7 @@ void Boiler::process_UBAErrorMessage(const std::shared_ptr & tel if (telegram->offset > 0 || telegram->message_length < 11) { return; } + // data: displaycode(2), errornumber(2), year, month, hour, day, minute, duration(2), src-addr static uint32_t lastCodeDate_ = 0; // last code date uint8_t code[3] = {telegram->message_data[0], telegram->message_data[1], 0}; @@ -1992,8 +2021,10 @@ void Boiler::process_UBAErrorMessage(const std::shared_ptr & tel has_update(lastCode_, newCode, sizeof(lastCode_)); lastCodeDate_ = 1; } + return; } + // store only the newest code from telegrams 10 and 11 if (date > lastCodeDate_) { lastCodeDate_ = date; @@ -2063,6 +2094,7 @@ void Boiler::process_UBAErrorMessage2(const std::shared_ptr & te snprintf(&code[3], sizeof(code) - 3, "(%d) @uptime %lu - %lu min", codeNo, starttime, endtime); date = starttime; } + if (date > lastCodeDate_) { lastCodeDate_ = date; has_update(lastCode_, code, sizeof(lastCode_)); @@ -2129,6 +2161,7 @@ void Boiler::process_UBAErrorMessage3(const std::shared_ptr & te snprintf(&code[3], sizeof(code) - 3, "(%d) @uptime %lu - %lu min", codeNo, starttime, endtime); date = starttime; } + if (date > lastCodeDate_) { lastCodeDate_ = date; has_update(lastCode_, code, sizeof(lastCode_)); @@ -2357,6 +2390,7 @@ bool Boiler::set_keepWarmTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x772, 1, v, 0x772); return true; } @@ -2366,6 +2400,7 @@ bool Boiler::set_returnTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x772, 2, v, 0x772); return true; } @@ -2529,6 +2564,7 @@ bool Boiler::set_ww_disinfect_duration(const char * value, const int8_t id) { if (!Helpers::value2number(value, v, 120, 240)) { // HPC410 menu: 120-240 min in steps of 10 return false; } + write_command(EMS_TYPE_UBAParameterWWPlus, 15, (v + 5) / 10, EMS_TYPE_UBAParameterWWPlus); return true; } @@ -2539,6 +2575,7 @@ bool Boiler::set_ww_prio(const char * value, const int8_t) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x2CC, 3, b ? 0xFF : 0, 0x2CC); return true; } @@ -2557,6 +2594,7 @@ bool Boiler::set_flow_temp(const char * value, const int8_t id) { if (has_telegram_id(0xE4)) { EMSESP::txservice_.add(Telegram::Operation::TX_WRITE, device_id(), EMS_TYPE_UBASetPoints2, 1, &v1, 1, 0, false); } + // always write to 0x1A EMSESP::txservice_.add(Telegram::Operation::TX_WRITE, device_id(), EMS_TYPE_UBASetPoints, 0, &v1, 1, 0, false); return true; @@ -2569,6 +2607,7 @@ bool Boiler::set_flow_temp(const char * value, const int8_t id) { } else { write_command(EMS_TYPE_UBASetPoints, 0, v, 0x18); } + return true; } @@ -2585,6 +2624,7 @@ bool Boiler::set_burn_power(const char * value, const int8_t id) { } else { write_command(EMS_TYPE_UBASetPoints, 1, v); } + return true; } @@ -2778,6 +2818,7 @@ bool Boiler::set_pumpMode(const char * value, const int8_t id) { write_command(EMS_TYPE_UBAParameters, 11, v, EMS_TYPE_UBAParameters); return true; } + return false; } @@ -2787,6 +2828,7 @@ bool Boiler::set_pumpCharacter(const char * value, const int8_t id) { write_command(EMS_TYPE_UBAParametersPlus, 15, v, EMS_TYPE_UBAParametersPlus); return true; } + return false; } @@ -2889,6 +2931,7 @@ bool Boiler::set_ww_mode(const char * value, const int8_t id) { return true; } } + return false; } @@ -3074,6 +3117,7 @@ bool Boiler::set_reset(const char * value, const int8_t id) { write_command(0x05, 6, 154); return true; } + return false; } @@ -3082,6 +3126,7 @@ bool Boiler::set_manDefrost(const char * value, const int8_t) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x05, 52, b ? 1 : 0); return true; } @@ -3091,6 +3136,7 @@ bool Boiler::set_chimneysweeper(const char * value, const int8_t) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x05, 0x04, b ? 100 : 0); return true; } @@ -3122,6 +3168,7 @@ bool Boiler::set_maintenance(const char * value, const int8_t id) { LOG_WARNING("Setting maintenance: wrong format %d.%d.%d", day, month, year + 2000); return false; } + return true; } @@ -3145,6 +3192,7 @@ bool Boiler::set_maintenance(const char * value, const int8_t id) { LOG_WARNING("Setting maintenance: wrong format"); return false; } + //maintenance bool Boiler::set_maintenancetime(const char * value, const int8_t id) { int hrs; @@ -3156,6 +3204,7 @@ bool Boiler::set_maintenancetime(const char * value, const int8_t id) { return true; } } + LOG_WARNING("Setting maintenance: wrong format"); return false; } @@ -3174,6 +3223,7 @@ bool Boiler::set_maintenancedate(const char * value, const int8_t id) { LOG_WARNING("Setting maintenance: wrong format %d.%d.%d", day, month, year + 2000); return false; } + return true; } @@ -3187,6 +3237,7 @@ bool Boiler::set_pool_temp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + uint8_t v2 = ((v * 2) + 0.5); // LOG_INFO("Setting pool temperature to %d.%d C", v2 >> 1, (v2 & 0x01) * 5); write_command(0x48A, 1, v2, 0x48A); @@ -3200,6 +3251,7 @@ bool Boiler::set_pool_on(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x48A, 0, v ? 0x01 : 0x00, 0x48A); return true; } @@ -3220,6 +3272,7 @@ bool Boiler::set_emergency_ops(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + write_command(EMS_TYPE_UBAParametersPlus, 18, v ? 0x01 : 0x00, EMS_TYPE_UBAParametersPlus); return true; } @@ -3228,11 +3281,13 @@ bool Boiler::set_HpInLogic(const char * value, const int8_t id) { if (id == 0 || id > 4 || strlen(value) > 15) { return false; } + bool v; if (strlen(value) == 1 && Helpers::value2bool(value, v)) { write_command(0x486, id == 4 ? 42 : id - 1, v ? 1 : 0, 0x486); return true; } + char option[] = {"xxxxxxxxxxxxxxx"}; strncpy(option, value, strlen(option)); // copy without termination // inputs 1,2,3 [] @@ -3243,9 +3298,11 @@ bool Boiler::set_HpInLogic(const char * value, const int8_t id) { write_command(0x486, index[i] + id - 1, option[i] - '0'); } } + if (option[12] >= '0' && option[12] <= '9') { write_command(0x486, 27, (uint8_t)atoi(&option[12])); } + return true; } @@ -3256,9 +3313,11 @@ bool Boiler::set_HpInLogic(const char * value, const int8_t id) { write_command(0x486, index4[i], option[i] - '0'); } } + if (option[9] >= '0' && option[9] <= '9') { write_command(0x486, 48, (uint8_t)atoi(&option[9])); } + return true; } @@ -3279,6 +3338,7 @@ bool Boiler::set_maxHeat(const char * value, const int8_t id) { return false; } } + write_command(0x492, id, v, 0x492); return true; } @@ -3288,6 +3348,7 @@ bool Boiler::set_silentMode(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_silentMode))) { return false; } + write_command(0x484, 10, v, 0x484); return true; } @@ -3296,6 +3357,7 @@ bool Boiler::set_silentFrom(const char * value, const int8_t id) { if (value == nullptr || value[0] < '0' || value[0] > '9') { return false; } + auto v = Helpers::string2minutes(value); write_command(0x484, 52, v / 15, 0x484); return true; @@ -3305,6 +3367,7 @@ bool Boiler::set_silentTo(const char * value, const int8_t id) { if (value == nullptr || value[0] < '0' || value[0] > '9') { return false; } + auto v = Helpers::string2minutes(value); write_command(0x484, 53, v / 15, 0x484); return true; @@ -3316,6 +3379,7 @@ bool Boiler::set_minTempSilent(const char * value, const int8_t id) { write_command(0x484, 11, v, 0x484); return true; } + return false; } @@ -3325,6 +3389,7 @@ bool Boiler::set_additionalHeaterOnly(const char * value, const int8_t id) { write_command(0x491, model() == EMSdevice::EMS_DEVICE_FLAG_CS6800 ? 3 : 1, v ? 1 : 0, 0x491); return true; } + return false; } @@ -3334,6 +3399,7 @@ bool Boiler::set_additionalHeater(const char * value, const int8_t id) { write_command(0x491, 2, v ? 1 : 0, 0x491); return true; } + return false; } @@ -3343,6 +3409,7 @@ bool Boiler::set_tempParMode(const char * value, const int8_t id) { write_command(0x491, 5, v, 0x491); return true; } + return false; } @@ -3354,6 +3421,7 @@ bool Boiler::set_additionalHeaterDelay(const char * value, const int8_t id) { write_command(0x491, 16, data, 2, 0x491); return true; } + return false; } @@ -3363,6 +3431,7 @@ bool Boiler::set_auxHeatMode(const char * value, const int8_t id) { write_command(0x491, 4, v, 0x491); return true; } + return false; } @@ -3372,6 +3441,7 @@ bool Boiler::set_auxLimit(const char * value, const int8_t id) { write_command(0x491, id, (uint8_t)(v * 10), 0x491); return true; } + return false; } @@ -3381,6 +3451,7 @@ bool Boiler::set_auxHeaterSource(const char * value, const int8_t) { write_command(0x491, 0, v, 0x491); return true; } + return false; } @@ -3390,6 +3461,7 @@ bool Boiler::set_pvCooling(const char * value, const int8_t id) { write_command(0x485, 21, v ? 1 : 0, 0x485); return true; } + return false; } @@ -3401,6 +3473,7 @@ bool Boiler::set_hpHyst(const char * value, const int8_t id) { write_command(0x484, id, data, 2, 0x484); return true; } + return false; } @@ -3410,6 +3483,7 @@ bool Boiler::set_tempDiff(const char * value, const int8_t id) { write_command(0x48B, id, (uint8_t)(v * 10), 0x48B); return true; } + return false; } @@ -3420,6 +3494,7 @@ bool Boiler::set_wwOffTemp(const char * value, const int8_t id) { write_command(0x499, id, v, 0x499); return true; } + return false; } @@ -3429,6 +3504,7 @@ bool Boiler::set_wwDiffTemp(const char * value, const int8_t id) { write_command(0x499, id, v, 0x499); return true; } + return false; } @@ -3438,6 +3514,7 @@ bool Boiler::set_hpCircPumpWw(const char * value, const int8_t id) { write_command(0x484, 46, v ? 1 : 0, 0x484); return true; } + return false; } @@ -3447,6 +3524,7 @@ bool Boiler::set_hpPumpMode(const char * value, const int8_t id) { write_command(0x48B, 18, v, 0x48B); return true; } + return false; } @@ -3456,6 +3534,7 @@ bool Boiler::set_hpMaxPower(const char * value, const int8_t id) { write_command(0x484, 31, v, 0x484); return true; } + return false; } @@ -3465,6 +3544,7 @@ bool Boiler::set_pvMaxComp(const char * value, const int8_t) { write_command(0x484, 54, (uint8_t)(v * 10), 0x484); return true; } + return false; } @@ -3474,6 +3554,7 @@ bool Boiler::set_hpDiffPress(const char * value, const int8_t id) { write_command(0x2CC, 9, (uint8_t)(v / 50), 0x2CC); return true; } + return false; } @@ -3484,6 +3565,7 @@ bool Boiler::set_hpPowerLimit(const char * value, const int8_t id) { write_command(0x4A7, 0, data, 2, 0x4A7); return true; } + return false; } @@ -3493,6 +3575,7 @@ bool Boiler::set_powerReduction(const char * value, const int8_t) { write_command(0x484, 64, v / 10, 0x484); return true; } + return false; } @@ -3502,6 +3585,7 @@ bool Boiler::set_vp_cooling(const char * value, const int8_t id) { write_command(0x49C, 3, v ? 1 : 0, 0x49C); return true; } + return false; } @@ -3511,6 +3595,7 @@ bool Boiler::set_heatCable(const char * value, const int8_t id) { write_command(0x49D, 2, v ? 1 : 0, 0x49D); return true; } + return false; } @@ -3520,6 +3605,7 @@ bool Boiler::set_VC0valve(const char * value, const int8_t id) { write_command(0x49D, 3, v ? 1 : 0, 0x49D); return true; } + return false; } @@ -3529,6 +3615,7 @@ bool Boiler::set_primePump(const char * value, const int8_t id) { write_command(0x49D, 4, v ? 1 : 0, 0x49D); return true; } + return false; } @@ -3538,6 +3625,7 @@ bool Boiler::set_primePumpMod(const char * value, const int8_t id) { write_command(0x49D, 5, v, 0x49D); return true; } + return false; } @@ -3547,6 +3635,7 @@ bool Boiler::set_hp3wayValve(const char * value, const int8_t id) { write_command(0x49D, 6, v ? 1 : 0, 0x49D); return true; } + return false; } @@ -3556,6 +3645,7 @@ bool Boiler::set_elHeatStep(const char * value, const int8_t id) { write_command(0x49D, 6 + id, v ? 1 : 0, 0x49D); return true; } + return false; } @@ -3565,6 +3655,7 @@ bool Boiler::set_wwAlternatingOper(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + write_command(EMS_TYPE_UBAParameterWWPlus, 14, v ? 1 : 0, EMS_TYPE_UBAParameterWWPlus); return true; } @@ -3576,6 +3667,7 @@ bool Boiler::set_wwAltOpPrio(const char * value, const int8_t id) { write_command(0x484, id, v, 0x484); return true; } + return false; } @@ -3588,15 +3680,18 @@ bool Boiler::set_forceHeatingOff(const char * value, const int8_t id) { uint8_t data[] = {1, heatingTemp_, (Helpers::hasValue(burnMaxPower_) ? burnMaxPower_ : (uint8_t)100), 1, 1}; write_command(EMS_TYPE_UBASetPoints2, 0, data, sizeof(data), 0); } + uint8_t data[] = {heatingTemp_, (Helpers::hasValue(burnMaxPower_) ? burnMaxPower_ : (uint8_t)100), (Helpers::hasValue(pumpModMax_) ? pumpModMax_ : (uint8_t)100), 0}; write_command(EMS_TYPE_UBASetPoints, 0, data, sizeof(data), 0); } + has_update(forceHeatingOff_, v); return true; } + return false; } @@ -3606,10 +3701,12 @@ bool Boiler::set_nrgHeat(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + if (v >= 0) { nrgHeatF_ = v * 100; has_update(nrgHeat_, (uint32_t)(nrgHeatF_ + 0.5)); } + store_energy(); return true; } @@ -3619,10 +3716,12 @@ bool Boiler::set_nrgWw(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + if (v >= 0) { nrgWwF_ = v * 100; has_update(nrgWw_, (uint32_t)(nrgWwF_ + 0.5)); } + store_energy(); return true; } @@ -3632,9 +3731,11 @@ bool Boiler::set_nomPower(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + if (v > 0 && v < EMS_VALUE_UINT8_NOTSET) { has_update(nomPower_, (uint8_t)v); } + store_energy(); return true; } @@ -3645,6 +3746,7 @@ bool Boiler::set_curveOn(const char * value, const int8_t id) { write_command(0x28, 0, v ? 0xFF : 0); return true; } + return false; } @@ -3653,6 +3755,7 @@ bool Boiler::set_curveBase(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x28, 2, v); return true; } @@ -3662,6 +3765,7 @@ bool Boiler::set_curveEnd(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x28, 1, v); return true; } @@ -3671,6 +3775,7 @@ bool Boiler::set_summerTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x28, 3, v); return true; } @@ -3681,6 +3786,7 @@ bool Boiler::set_nofrost(const char * value, const int8_t id) { write_command(0x28, 4, v ? 0xFF : 0); return true; } + return false; } @@ -3689,6 +3795,7 @@ bool Boiler::set_nofrostTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x28, 5, v); return true; } @@ -3699,6 +3806,7 @@ bool Boiler::set_fan(const char * value, const int8_t id) { write_command(0x4A5, 9, (uint8_t)v, 0x4A5); return true; } + return false; } @@ -3708,6 +3816,7 @@ bool Boiler::set_shutdown(const char * value, const int8_t id) { write_command(0x484, 58, b ? 1 : 0, 0x484); return true; } + return false; } @@ -3717,6 +3826,7 @@ bool Boiler::set_pumpKickHour(const char * value, const int8_t) { write_command(0xEB, 0, v, 0xEB); return true; } + return false; } @@ -3726,6 +3836,7 @@ bool Boiler::set_pumpKickDay(const char * value, const int8_t) { write_command(0xEB, 1, v + 1, 0xEB); return true; } + return false; } @@ -3736,6 +3847,7 @@ bool Boiler::set_pumpKickDelay(const char * value, const int8_t) { write_command(0xEB, 2, data, 2, 0xEB); return true; } + return false; } diff --git a/src/devices/boiler.h b/src/devices/boiler.h index 165b2e245..90517e9e2 100644 --- a/src/devices/boiler.h +++ b/src/devices/boiler.h @@ -34,6 +34,7 @@ class Boiler : public EMSdevice { inline uint8_t model() const { return (flags() & 0x0F); } + inline bool isHeatPump() const { return flags() & 0x08; } @@ -454,9 +455,11 @@ class Boiler : public EMSdevice { inline bool set_hyst2_on(const char * value, const int8_t id) { return set_hyst_on(value, 2); } + inline bool set_hyst2_off(const char * value, const int8_t id) { return set_hyst_off(value, 2); } + bool set_burn_period(const char * value, const int8_t id); bool set_pump_delay(const char * value, const int8_t id); bool set_reset(const char * value, const int8_t id); @@ -476,25 +479,32 @@ class Boiler : public EMSdevice { inline bool set_HpIn1Logic(const char * value, const int8_t id) { return set_HpInLogic(value, 1); } + inline bool set_HpIn2Logic(const char * value, const int8_t id) { return set_HpInLogic(value, 2); } + inline bool set_HpIn3Logic(const char * value, const int8_t id) { return set_HpInLogic(value, 3); } + inline bool set_HpIn4Logic(const char * value, const int8_t id) { return set_HpInLogic(value, 4); } + bool set_maxHeat(const char * value, const int8_t id); inline bool set_maxHeatComp(const char * value, const int8_t id) { return set_maxHeat(value, 2); } + inline bool set_maxHeatHeat(const char * value, const int8_t id) { return set_maxHeat(value, 3); } + inline bool set_maxHeatDhw(const char * value, const int8_t id) { return set_maxHeat(value, 4); } + bool set_silentMode(const char * value, const int8_t id); bool set_minTempSilent(const char * value, const int8_t id); bool set_silentFrom(const char * value, const int8_t id); @@ -518,55 +528,71 @@ class Boiler : public EMSdevice { inline bool set_auxMaxLimit(const char * value, const int8_t id) { return set_auxLimit(value, 14); } + inline bool set_auxLimitStart(const char * value, const int8_t id) { return set_auxLimit(value, 15); } + bool set_hpHyst(const char * value, const int8_t id); inline bool set_hpHystHeat(const char * value, const int8_t id) { return set_hpHyst(value, 37); } + inline bool set_hpHystCool(const char * value, const int8_t id) { return set_hpHyst(value, 35); } + inline bool set_hpHystPool(const char * value, const int8_t id) { return set_hpHyst(value, 33); } + bool set_tempDiff(const char * value, const int8_t id); inline bool set_tempDiffHeat(const char * value, const int8_t id) { return set_tempDiff(value, 4); } + inline bool set_tempDiffCool(const char * value, const int8_t id) { return set_tempDiff(value, 3); } + bool set_wwOffTemp(const char * value, const int8_t id); inline bool set_wwComfOffTemp(const char * value, const int8_t id) { return set_wwOffTemp(value, 1); } + inline bool set_wwEcoOffTemp(const char * value, const int8_t id) { return set_wwOffTemp(value, 0); } + inline bool set_wwEcoPlusOffTemp(const char * value, const int8_t id) { return set_wwOffTemp(value, 5); } + bool set_wwDiffTemp(const char * value, const int8_t id); inline bool set_wwComfDiffTemp(const char * value, const int8_t id) { return set_wwDiffTemp(value, 12); } + inline bool set_wwEcoDiffTemp(const char * value, const int8_t id) { return set_wwDiffTemp(value, 13); } + inline bool set_wwEcoPlusDiffTemp(const char * value, const int8_t id) { return set_wwDiffTemp(value, 14); } + inline bool set_wwComfStopTemp(const char * value, const int8_t id) { return set_wwOffTemp(value, 8); } + inline bool set_wwEcoStopTemp(const char * value, const int8_t id) { return set_wwOffTemp(value, 9); } + inline bool set_wwEcoPlusStopTemp(const char * value, const int8_t id) { return set_wwOffTemp(value, 10); } + bool set_vp_cooling(const char * value, const int8_t id); bool set_heatCable(const char * value, const int8_t id); bool set_VC0valve(const char * value, const int8_t id); @@ -577,20 +603,25 @@ class Boiler : public EMSdevice { inline bool set_elHeatStep1(const char * value, const int8_t id) { return set_elHeatStep(value, 1); } + inline bool set_elHeatStep2(const char * value, const int8_t id) { return set_elHeatStep(value, 2); } + inline bool set_elHeatStep3(const char * value, const int8_t id) { return set_elHeatStep(value, 3); } + bool set_wwAlternatingOper(const char * value, const int8_t id); bool set_wwAltOpPrio(const char * value, const int8_t id); inline bool set_wwAltOpPrioHeat(const char * value, const int8_t id) { return set_wwAltOpPrio(value, 2); } + inline bool set_wwAltOpPrioWw(const char * value, const int8_t id) { return set_wwAltOpPrio(value, 3); } + bool set_forceHeatingOff(const char * value, const int8_t id); /* @@ -622,4 +653,4 @@ class Boiler : public EMSdevice { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/devices/connect.cpp b/src/devices/connect.cpp index 9628d54a0..84cc3b481 100644 --- a/src/devices/connect.cpp +++ b/src/devices/connect.cpp @@ -27,6 +27,7 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con if (device_id != EMSdevice::EMS_DEVICE_ID_RFBASE) { // Modems have no entities return; } + register_telegram_type(0xD1, "RFOutdoorTemp", false, MAKE_PF_CB(process_OutdoorTemp)); register_telegram_type(0x06, "RCTime", false, MAKE_PF_CB(process_RCTime)); @@ -45,6 +46,7 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con register_telegram_type(0x1230 + i, "Roomparams", false, MAKE_PF_CB(process_roomThermostatParam)); // fetch for active circuits register_telegram_type(0x1244 + i, "Roomdata", false, MAKE_PF_CB(process_roomThermostatData)); // broadcasted } + register_telegram_type(0x0B65, "Roomschedule", true, MAKE_PF_CB(process_roomSchedule)); register_telegram_type(0xF7, "MenuConfig", false, MAKE_PF_CB(process_roomConfig)); // 0x2040, broadcast 36 bytes: @@ -57,6 +59,7 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con register_device_values_room(new_room); #endif } + /* * OutdoorTemp - type 0xD1 - external temperature */ @@ -70,6 +73,7 @@ void Connect::process_RCTime(const std::shared_ptr & telegram) { if (telegram->offset || telegram->message_length < 10) { return; } + char time[sizeof(dateTime_)]; snprintf(time, sizeof(time), @@ -106,9 +110,11 @@ std::shared_ptr Connect::room_circuit(const uint8_t num, c return room_circuit; } } + if (!create) { return nullptr; } + // create a new circuit object and add to the list auto new_room = std::make_shared(num); room_circuits_.push_back(new_room); @@ -128,12 +134,14 @@ void Connect::process_roomThermostat(const std::shared_ptr & tel if (rc == nullptr) { return; } + has_update(telegram, rc->temp_, 0); has_update(telegram, rc->humidity_, 2); // could show -3 if not set // 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 has_update(rc->dewtemp_, Helpers::calc_dew(rc->temp_, rc->humidity_)); } @@ -145,12 +153,14 @@ void Connect::process_roomThermostatName(const std::shared_ptr & if (rc == nullptr) { return; } + has_update(telegram, rc->icon_, 0); for (uint8_t i = telegram->offset; i < telegram->message_length + telegram->offset && i < 100; i++) { if ((i > 1) && (i % 2) == 0) { rc->name_[(i - 2) / 2] = telegram->message_data[i - telegram->offset]; } } + rc->name_[50] = '\0'; // make sure name is terminated } @@ -161,6 +171,7 @@ void Connect::process_roomThermostatSettings(const std::shared_ptrmode_, 0, {3, 1, 0}); // modes off, manual auto // has_update(telegram, rc->mode_, 0); // modes: auto, heat, cool, off // has_update(telegram, rc->tempautotemp_, 1); // FF means off @@ -215,6 +226,7 @@ void Connect::process_roomSchedule(const std::shared_ptr & teleg return; } } + toggle_fetch(telegram->type_id, false); // fetch only once if all is initialized } @@ -227,6 +239,7 @@ void Connect::process_roomConfig(const std::shared_ptr & telegra if (rc == nullptr) { return; } + uint8_t bits = telegram->message_data[3]; if (rc->mode_ == 2) { if (bits & 0x02) { @@ -235,6 +248,7 @@ void Connect::process_roomConfig(const std::shared_ptr & telegra rc->coolmode_ = 1; } } + /* switch (bits & 0x6A) { case 0: // cooling off @@ -272,12 +286,14 @@ bool Connect::set_mode(const char * value, const int8_t id) { if (rc == nullptr) { return false; } + uint8_t v; if (!Helpers::value2enum(value, v, FL_(enum_mode2), {3, 1, 0})) { if (!Helpers::value2enum(value, v, FL_(enum_mode_ha), {3, 1, 0})) { return false; } } + write_command(0xBB5 + rc->room(), rc->coolmode_ == 1 ? 4 : 0, v); // no validate, mode change is broadcasted return true; } @@ -287,6 +303,7 @@ bool Connect::set_seltemp(const char * value, const int8_t id) { if (rc == nullptr) { return false; } + float v; if (Helpers::value2temperature(value, v)) { // depends on mode, auto (2 for enum_mode2, 0 for enum_mode8) set in offset 1 @@ -295,8 +312,10 @@ bool Connect::set_seltemp(const char * value, const int8_t id) { } else { write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); } + return true; } + return false; } @@ -305,6 +324,7 @@ bool Connect::set_name(const char * value, const int8_t id) { if (rc == nullptr || value == nullptr || strlen(value) > 50) { return false; } + Helpers::utf8tolatin1(rc->name_, value, sizeof(rc->name_)); uint8_t len = strlen(rc->name_) * 2 + 2; uint8_t data[len]; @@ -312,6 +332,7 @@ bool Connect::set_name(const char * value, const int8_t id) { data[2 * i] = 0; data[2 * i + 1] = rc->name_[i]; } + uint8_t ofs = 0; while (len > 0) { uint8_t part = len > 25 ? 25 : len; @@ -319,6 +340,7 @@ bool Connect::set_name(const char * value, const int8_t id) { ofs += part; len -= part; } + return true; } @@ -327,11 +349,13 @@ bool Connect::set_childlock(const char * value, const int8_t id) { if (rc == nullptr) { return false; } + bool b; if (Helpers::value2bool(value, b)) { write_command(0xBB5 + rc->room(), 7, b ? 1 : 0, 0xBB5 + rc->room()); return true; } + return false; } @@ -340,11 +364,13 @@ bool Connect::set_icon(const char * value, const int8_t id) { if (rc == nullptr) { return false; } + uint8_t v; if (Helpers::value2enum(value, v, FL_(enum_icons))) { write_command(0x0B3D + rc->room(), 0, v, 0x0B3D + rc->room()); return true; } + return false; } diff --git a/src/devices/connect.h b/src/devices/connect.h index a952e9090..ff76f7753 100644 --- a/src/devices/connect.h +++ b/src/devices/connect.h @@ -32,6 +32,7 @@ class Connect : public EMSdevice { RoomCircuit(const uint8_t num) : room_(num) { } + ~RoomCircuit() = default; int16_t temp_; uint8_t humidity_; diff --git a/src/devices/controller.cpp b/src/devices/controller.cpp index 1b2227bc0..a6233fffd 100644 --- a/src/devices/controller.cpp +++ b/src/devices/controller.cpp @@ -36,6 +36,7 @@ void Controller::process_dateTime(const std::shared_ptr & telegr if (telegram->offset > 0 || telegram->message_length < 5) { return; } + char newdatetime[sizeof(dateTime_)]; // publish as dd.mm.yyyy hh:mm snprintf(newdatetime, @@ -49,4 +50,4 @@ void Controller::process_dateTime(const std::shared_ptr & telegr has_update(dateTime_, newdatetime, sizeof(dateTime_)); } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/devices/extension.cpp b/src/devices/extension.cpp index 17b8bda67..268b8e853 100644 --- a/src/devices/extension.cpp +++ b/src/devices/extension.cpp @@ -28,6 +28,7 @@ Extension::Extension(uint8_t device_type, uint8_t device_id, uint8_t product_id, // no entities for T1RF outdoor sensor, values are coming from RFbase 0x50 (connect) return; } + // Extension module EM100 device_id 0x12 register_telegram_type(0x935, "EM100SetMessage", true, MAKE_PF_CB(process_EM100SetMessage), 5); register_telegram_type(0x936, "EM100OutMessage", false, MAKE_PF_CB(process_EM100OutMessage)); @@ -108,6 +109,7 @@ bool Extension::set_minV(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + write_command(0x935, 1, (uint8_t)(v * 10)); return true; } @@ -117,6 +119,7 @@ bool Extension::set_maxV(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + write_command(0x935, 2, (uint8_t)(v * 10)); return true; } @@ -126,6 +129,7 @@ bool Extension::set_minT(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x935, 3, v); return true; } @@ -135,6 +139,7 @@ bool Extension::set_maxT(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x935, 4, v); return true; } diff --git a/src/devices/heatpump.cpp b/src/devices/heatpump.cpp index 6abe6918e..81cbfd5b3 100644 --- a/src/devices/heatpump.cpp +++ b/src/devices/heatpump.cpp @@ -381,119 +381,148 @@ bool Heatpump::set_controlStrategy(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_hybridStrategy1))) { return false; } + write_command(0x998, 0, v, 0x998); return true; } + bool Heatpump::set_lowNoiseMode(const char * value, const int8_t id) { uint8_t v; if (!Helpers::value2enum(value, v, FL_(enum_lowNoiseMode))) { return false; } + write_command(0x998, 9, v, 0x998); return true; } + bool Heatpump::set_lowNoiseStart(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v, 0, 23)) { return false; } + write_command(0x998, 10, v, 0x998); return true; } + bool Heatpump::set_lowNoiseStop(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v, 0, 23)) { return false; } + write_command(0x998, 11, v, 0x998); return true; } + bool Heatpump::set_hybridDHW(const char * value, const int8_t id) { uint8_t v; if (!Helpers::value2enum(value, v, FL_(enum_comfort2))) { return false; } + write_command(0x998, 1, v, 0x998); return true; } + bool Heatpump::set_energyPriceGas(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v)) { return false; } + write_command(0x998, 2, v, 0x998); return true; } + bool Heatpump::set_energyPriceEl(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v)) { return false; } + write_command(0x998, 5, v, 0x998); return true; } + bool Heatpump::set_energyPricePV(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v)) { return false; } + write_command(0x998, 6, v, 0x998); return true; } + bool Heatpump::set_switchOverTemp(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v)) { return false; } + write_command(0x998, 14, v, 0x998); return true; } + // Function tests: bool Heatpump::set_airPurgeMode(const char * value, const int8_t id) { bool v; if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x999, 0, v, 0x999); return true; } + bool Heatpump::set_heatPumpOutput(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v, 0, 100)) { return false; } + write_command(0x999, 2, v, 0x999); return true; } + bool Heatpump::set_coolingCircuit(const char * value, const int8_t id) { bool v; if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x999, 6, v, 0x999); return true; } + bool Heatpump::set_compStartMod(const char * value, const int8_t id) { int v; if (!Helpers::value2number(value, v, 0, 100)) { return false; } + write_command(0x999, 7, v, 0x999); return true; } + bool Heatpump::set_heatDrainPan(const char * value, const int8_t id) { bool v; if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x999, 9, v, 0x999); return true; } + bool Heatpump::set_heatCable(const char * value, const int8_t id) { bool v; if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x999, 10, v, 0x999); return true; } diff --git a/src/devices/heatsource.cpp b/src/devices/heatsource.cpp index 77f558ca1..eeef8bdbb 100644 --- a/src/devices/heatsource.cpp +++ b/src/devices/heatsource.cpp @@ -187,6 +187,7 @@ bool Heatsource::set_vr2Config(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_vr2Config))) { return false; } + write_command(0x54C, 12, v, 0x54C); return true; } @@ -197,6 +198,7 @@ bool Heatsource::set_ahsActivated(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x54C, 0, v, 0x54C); return true; } @@ -207,6 +209,7 @@ bool Heatsource::set_aPumpConfig(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x54C, 4, v, 0x54C); return true; } @@ -217,6 +220,7 @@ bool Heatsource::set_aPumpSignal(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_aPumpSignal))) { return false; } + write_command(0x54C, 3, v, 0x54C); return true; } @@ -227,6 +231,7 @@ bool Heatsource::set_aPumpMin(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + write_command(0x54C, 21, v, 0x54C); return true; } @@ -237,6 +242,7 @@ bool Heatsource::set_tempRise(const char * value, const int8_t id) { if (!Helpers::value2bool(value, v)) { return false; } + write_command(0x54C, 1, v, 0x54C); return true; } @@ -247,6 +253,7 @@ bool Heatsource::set_setReturnTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0x54C, 6, v, 0x54C); return true; } @@ -257,6 +264,7 @@ bool Heatsource::set_mixRuntime(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + uint8_t data[2] = {(uint8_t)(v >> 8), (uint8_t)v}; write_command(0x54C, 8, data, 2, 0x54C); return true; @@ -268,6 +276,7 @@ bool Heatsource::set_setFlowTemp(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + write_command(0x54C, 7, v, 0x54C); return true; } @@ -278,6 +287,7 @@ bool Heatsource::set_bufBypass(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_bufBypass))) { return false; } + write_command(0x54C, 2, v, 0x54C); return true; } @@ -288,6 +298,7 @@ bool Heatsource::set_bufMixRuntime(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + uint8_t data[2] = {(uint8_t)(v >> 8), (uint8_t)v}; write_command(0x54C, 10, data, 2, 0x54C); return true; @@ -299,6 +310,7 @@ bool Heatsource::set_bufConfig(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_bufConfig))) { return false; } + write_command(0x54C, 20, v, 0x54C); return true; } @@ -309,6 +321,7 @@ bool Heatsource::set_blockMode(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_blockMode))) { return false; } + write_command(0x54C, 16, v, 0x54C); return true; } @@ -319,6 +332,7 @@ bool Heatsource::set_blockTerm(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_blockTerm))) { return false; } + write_command(0x54C, 17, v, 0x54C); return true; } @@ -329,6 +343,7 @@ bool Heatsource::set_blockHyst(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v, true)) { return false; } + write_command(0x54C, 14, v, 0x54C); return true; } @@ -339,6 +354,7 @@ bool Heatsource::set_releaseWait(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + write_command(0x54C, 15, v, 0x54C); return true; } @@ -363,4 +379,4 @@ bool Heatsource::set_valveReturn(const char * value, const int8_t id) { } */ -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/devices/mixer.cpp b/src/devices/mixer.cpp index 22ff972ac..51af9defd 100644 --- a/src/devices/mixer.cpp +++ b/src/devices/mixer.cpp @@ -190,19 +190,23 @@ bool Mixer::set_flowSetTemp(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) { write_command(0xAC, 0, v, 0xAB); return true; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { write_command(0x123, 0, v, 0x10C); return true; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { uint8_t hc = device_id() - 0x20; write_command(0x2E1 + hc, 1, v, 0x2D7 + hc); return true; } + return false; } @@ -211,21 +215,25 @@ bool Mixer::set_pump(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) { // AC telegram can only be written with offset 0 uint8_t dat[2] = {flowSetTemp_, b ? (uint8_t)0x64 : (uint8_t)0}; write_command(0xAC, 0, dat, sizeof(dat), 0xAB); return true; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { write_command(0x123, 1, b ? 0x64 : 0, 0x10C); return true; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { uint8_t hc = device_id() - 0x20; write_command(0x2E1 + hc, 2, b ? 0x64 : 0, 0x2D7 + hc); return true; } + return false; } @@ -234,15 +242,18 @@ bool Mixer::set_activated(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) { write_command(0xAA, 0, b ? 0xFF : 0, 0xAA); return true; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { uint8_t hc = device_id() - 0x20; write_command(0x2CD + hc, 0, b ? 0xFF : 0, 0x2CD + hc); return true; } + return false; } @@ -251,17 +262,20 @@ bool Mixer::set_setValveTime(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MM10) { v = (v + 5) / 10; write_command(0xAA, 1, v, 0xAA); return true; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { v = (v + 5) / 10; uint8_t hc = device_id() - 0x20; write_command(0x2CD + hc, 1, v, 0x2CD + hc); return true; } + return false; } @@ -270,11 +284,13 @@ bool Mixer::set_flowTempOffset(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { uint8_t hc = device_id() - 0x20; write_command(0x2CD + hc, 2, v, 0x2CD + hc); return true; } + return false; } @@ -283,6 +299,7 @@ bool Mixer::set_pressure(const char * value, const int8_t) { if (!Helpers::value2number(value, v)) { return false; } + uint8_t hc = device_id() - 0x20; write_command(0x2CD + hc, 9, v / 50, 0x2CD + hc); return true; @@ -293,6 +310,7 @@ bool Mixer::set_wwprio(const char * value, const int8_t) { if (!Helpers::value2bool(value, b)) { return false; } + uint8_t hc = device_id() - 0x20; write_command(0x2CD + hc, 3, b ? 0xFF : 0, 0x2CD + hc); return true; diff --git a/src/devices/pool.cpp b/src/devices/pool.cpp index 361f9631c..1eec31cc9 100644 --- a/src/devices/pool.cpp +++ b/src/devices/pool.cpp @@ -50,6 +50,7 @@ void Pool::process_HpPoolStatus(const std::shared_ptr & telegram if (telegram->read_value(request, 4)) { has_update(poolRequest_, request == 0 ? EMS_VALUE_BOOL_ON : EMS_VALUE_BOOL_OFF); } + has_update(telegram, poolTemp_, 0); has_update(telegram, poolShunt_, 3); // 0-100%, 0% = pool / 100% = heat telegram->read_value(poolShuntStatus__, 2); diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index f2555dfab..e6d00a34a 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -137,6 +137,7 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const c register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &data0_, DeviceValueType::UINT8, FL_(data0), DeviceValueUOM::NONE); */ } + if (flags == EMSdevice::EMS_DEVICE_FLAG_ISM) { register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &cylMiddleTemp_, @@ -482,6 +483,7 @@ void Solar::process_SM10Monitor(const std::shared_ptr & telegram if (solarpumpmod == 100 && solarPumpMod_ == 0 && solarPumpMinMod_ > 0) { solarpumpmod = solarPumpMinMod_; // set to minimum } + has_update(solarPumpMod_, solarpumpmod); if (!Helpers::hasValue(maxFlow_)) { @@ -497,11 +499,13 @@ void Solar::process_SM10Monitor(const std::shared_ptr & telegram if (energy.size() >= 60) { energy.pop_front(); } + energy.push_back(solarPower_); int32_t sum = 0; for (auto e : energy) { sum += e; } + energyLastHour_ = sum > 0 ? sum / 6 : 0; // counts in 0.1 Wh has_update(&solarPower_); has_update(&energyLastHour_); @@ -667,10 +671,12 @@ void Solar::process_SM100Status(const std::shared_ptr & telegram if (solarpumpmod == 100 && solarPumpMod_ == 0 && solarPumpMinMod_ > 0) { solarpumpmod = solarPumpMinMod_ * 5; } + has_update(solarPumpMod_, solarpumpmod); if (cylinderpumpmod == 100 && cylPumpMod_ == 0 && solarPumpMinMod_ > 0) { cylinderpumpmod = solarPumpMinMod_ * 5; } + has_update(cylPumpMod_, cylinderpumpmod); has_bitupdate(telegram, cylHeated_, 3, 1); // issue #422 @@ -683,6 +689,7 @@ void Solar::process_SM100Status(const std::shared_ptr & telegram if (solarpumpmod == 100 && solarPump2Mod_ == 0 && solarPumpMinMod_ > 0) { solarpumpmod = solarPumpMinMod_ * 5; // set to minimum } + has_update(solarPump2Mod_, solarpumpmod); has_update(telegram, transferPumpMod_, 14); @@ -720,6 +727,7 @@ void Solar::process_SM100CollectorConfig(const std::shared_ptr & collector2Area_ = EMS_VALUE_UINT16_NOTSET; collector2Type_ = EMS_VALUE_UINT8_NOTSET; } + // has_enumupdate(telegram, collector2Type_, 8, 1); } @@ -795,11 +803,13 @@ bool Solar::set_CollectorMaxTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 3, (uint8_t)temperature, 0x96); } else { write_command(0x35A, 0, (uint8_t)temperature, 0x35A); } + return true; } @@ -809,11 +819,13 @@ bool Solar::set_CollectorMinTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 4, (uint8_t)temperature, 0x96); } else { write_command(0x35A, 4, (uint8_t)temperature, 0x35A); } + return true; } @@ -823,6 +835,7 @@ bool Solar::set_cylMaxTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 5, (uint8_t)temperature, 0x96); } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_ISM) { @@ -830,6 +843,7 @@ bool Solar::set_cylMaxTemp(const char * value, const int8_t id) { } else { write_command(0x35A, 3, (uint8_t)temperature, 0x35A); } + return true; } @@ -839,11 +853,13 @@ bool Solar::set_PumpMinMod(const char * value, const int8_t id) { if (!Helpers::value2number(value, modulation)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 2, (uint8_t)modulation, 0x96); } else { write_command(0x35A, 6, (uint8_t)(modulation + 2) / 5, 0x35A); } + return true; } @@ -853,6 +869,7 @@ bool Solar::set_Pump2MinMod(const char * value, const int8_t id) { if (!Helpers::value2number(value, modulation)) { return false; } + // write_command(0x35D, x, (uint8_t)(modulation), 0x35D); return true; } @@ -863,6 +880,7 @@ bool Solar::set_wwMinTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + write_command(0x96, 6, (uint8_t)temperature, 0x96); return true; } @@ -873,11 +891,13 @@ bool Solar::set_TurnoffDiff(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature, true)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 8, (uint8_t)temperature, 0x96); } else { write_command(0x35A, 7, (uint8_t)(temperature * 10), 0x35A); } + return true; } @@ -887,11 +907,13 @@ bool Solar::set_TurnonDiff(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature, true)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 7, (uint8_t)temperature, 0x96); } else { write_command(0x35A, 8, (uint8_t)(temperature * 10), 0x35A); } + return true; } @@ -901,6 +923,7 @@ bool Solar::set_TurnoffDiff2(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature, true)) { return false; } + write_command(0x361, 3, (uint8_t)(temperature * 10), 0x361); return true; } @@ -911,6 +934,7 @@ bool Solar::set_TurnonDiff2(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature, true)) { return false; } + write_command(0x361, 4, (uint8_t)(temperature * 10), 0x361); return true; } @@ -921,6 +945,7 @@ bool Solar::set_SM10MaxFlow(const char * value, const int8_t id) { if (!Helpers::value2float(value, flow)) { return false; } + maxFlow_ = (flow * 10); EMSESP::webSettingsService.update([&](WebSettings & settings) { settings.solar_maxflow = maxFlow_; @@ -935,6 +960,7 @@ bool Solar::set_heatTransferSystem(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x358, 5, b ? 0xFF : 0x00); // change is broadcasted return true; } @@ -945,6 +971,7 @@ bool Solar::set_externalCyl(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x358, 9, b ? 0xFF : 0x00); // change is broadcasted return true; } @@ -955,6 +982,7 @@ bool Solar::set_thermalDisinfect(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x358, 10, b ? 0xFF : 0x00); // change is broadcasted return true; } @@ -965,6 +993,7 @@ bool Solar::set_heatMetering(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x358, 14, b ? 0xFF : 0x00); // change is broadcasted return true; } @@ -975,11 +1004,13 @@ bool Solar::set_solarEnabled(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_SM10) { write_command(0x96, 0, b ? 0xFF : 0x00, 0x96); } else { write_command(0x358, 19, b ? 0xFF : 0x00); // change is broadcasted } + return true; } @@ -989,6 +1020,7 @@ bool Solar::set_solarMode(const char * value, const int8_t id) { if (!Helpers::value2enum(value, num, FL_(enum_solarmode))) { return false; } + write_command(0x35A, 5, num, 0x35A); return true; } @@ -1010,6 +1042,7 @@ bool Solar::set_solarPumpKick(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x35A, 9, b ? 0x01 : 0x00, 0x35A); return true; } @@ -1020,6 +1053,7 @@ bool Solar::set_solarPump2Kick(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x35D, 0, b ? 0x01 : 0x00, 0x35D); return true; } @@ -1030,6 +1064,7 @@ bool Solar::set_plainWaterMode(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x35A, 10, b ? 0x01 : 0x00, 0x35A); return true; } @@ -1040,6 +1075,7 @@ bool Solar::set_doubleMatchFlow(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x35A, 11, b ? 0x01 : 0x00, 0x35A); return true; } @@ -1050,6 +1086,7 @@ bool Solar::set_climateZone(const char * value, const int8_t id) { if (!Helpers::value2number(value, zone)) { return false; } + write_command(0x380, 0, zone, 0x380); return true; } @@ -1060,6 +1097,7 @@ bool Solar::set_collector1Area(const char * value, const int8_t id) { if (!Helpers::value2float(value, area)) { return false; } + write_command(0x380, 3, (uint16_t)(area * 10), 0x380); return true; } @@ -1070,6 +1108,7 @@ bool Solar::set_collector2Area(const char * value, const int8_t id) { if (!Helpers::value2float(value, area)) { return false; } + write_command(0x380, 6, (uint16_t)(area * 10), 0x380); return true; } @@ -1080,6 +1119,7 @@ bool Solar::set_collector1Type(const char * value, const int8_t id) { if (!Helpers::value2enum(value, num, FL_(enum_collectortype))) { return false; } + write_command(0x380, 5, num + 1, 0x380); return true; } @@ -1090,6 +1130,7 @@ bool Solar::set_collector2Type(const char * value, const int8_t id) { if (!Helpers::value2enum(value, num, FL_(enum_collectortype))) { return false; } + write_command(0x380, 8, num + 1, 0x380); return true; } @@ -1100,6 +1141,7 @@ bool Solar::set_cylPriority(const char * value, const int8_t id) { if (!Helpers::value2enum(value, num, FL_(enum_cylprio))) { return false; } + write_command(0x35F, 3, num, 0x35F); return true; } @@ -1109,6 +1151,7 @@ bool Solar::set_heatAssist(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + write_command(0x35C, 0, (uint8_t)(temperature * 10), 0x35C); return true; } @@ -1118,6 +1161,7 @@ bool Solar::set_diffControl(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + write_command(0x361, 4, (uint8_t)(temperature * 10), 0x361); return true; } @@ -1127,6 +1171,7 @@ bool Solar::set_solarHeatAssistOn(const char * value, const int8_t) { if (!Helpers::value2float(value, t)) { return false; } + write_command(0x35C, 0, (uint8_t)(t * 10), 0x35C); return true; } @@ -1136,6 +1181,7 @@ bool Solar::set_solarHeatAssistOff(const char * value, const int8_t) { if (!Helpers::value2float(value, t)) { return false; } + write_command(0x35C, 1, (uint8_t)(t * 10), 0x35C); return true; } diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index cbff6df76..38a94fcf7 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -38,6 +38,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_device_values(); return; } + // remote thermostats with humidity: RC100H remote, each thermostat is for one hc if (device_id >= 0x38 && device_id <= 0x3F) { register_telegram_type(0x042B + device_id - 0x38, "RemoteTemp", false, MAKE_PF_CB(process_RemoteTemp)); @@ -49,6 +50,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i // register_telegram_type(0x0273, "RemoteCorrection", true, MAKE_PF_CB(process_RemoteCorrection), 1); // register_telegram_type(0x0A6B, "RemoteBattery", true, MAKE_PF_CB(process_RemoteBattery), 2); } + register_device_values(); // register device values for common values (not heating circuit) return; // no values to add } @@ -85,6 +87,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(timer_typeids[i], "RC35Timer", false, MAKE_PF_CB(process_RC35Timer), 99); register_telegram_type(timer2_typeids[i], "RC35Timer2", false, MAKE_PF_CB(process_RC35Timer), 99); } + register_telegram_type(EMS_TYPE_IBASettings, "IBASettings", true, MAKE_PF_CB(process_IBASettings), 22); register_telegram_type(EMS_TYPE_wwSettings, "WWSettings", true, MAKE_PF_CB(process_RC35wwSettings), 10); register_telegram_type(0x38, "WWTimer", true, MAKE_PF_CB(process_RC35wwTimer), 99); @@ -108,6 +111,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i // remote thermostat uses only 0xAF register_telegram_type(0xAF, "RC20Remote", false, MAKE_PF_CB(process_RC20Remote)); } + // RC20 newer } else if ((model == EMSdevice::EMS_DEVICE_FLAG_RC20_N) || (model == EMSdevice::EMS_DEVICE_FLAG_RC25)) { if (device_id == 0x17) { // master @@ -122,6 +126,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i // remote thermostat uses only 0xAF register_telegram_type(0xAF, "RC20Remote", false, MAKE_PF_CB(process_RC20Remote)); } + // RC30 } else if (model == EMSdevice::EMS_DEVICE_FLAG_RC30) { monitor_typeids = {0x41}; @@ -135,6 +140,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(curve_typeids[i], "RC30Temp", false, MAKE_PF_CB(process_RC30Temp), 25); register_telegram_type(timer_typeids[i], "RC30Timer", false, MAKE_PF_CB(process_RC35Timer), 99); } + register_telegram_type(0xA9, "RC30Vacation", true, MAKE_PF_CB(process_RC30Vacation), 57); register_telegram_type(EMS_TYPE_RC30wwSettings, "RC30WWSettings", true, MAKE_PF_CB(process_RC30wwSettings), 5); register_telegram_type(0x38, "WWTimer", true, MAKE_PF_CB(process_RC35wwTimer), 99); @@ -184,6 +190,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(curve_typeids[i], "RC300Curves", false, MAKE_PF_CB(process_RC300Curve), 9); register_telegram_type(summer2_typeids[i], "RC300Summer2", false, MAKE_PF_CB(process_RC300Summer2), 8); } + const size_t set2_size = set2_typeids.size(); for (uint8_t i = 0; i < set2_size; i++) { // register_telegram_type(set2_typeids[i], "RC300Set2", false, MAKE_PF_CB(process_RC300Set2)); @@ -191,6 +198,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(hp_typeids[i], "HPSet", false, MAKE_PF_CB(process_HPSet), 5); register_telegram_type(hpmode_typeids[i], "HPMode", true, MAKE_PF_CB(process_HPMode), 6); } + register_telegram_type(0x2F5, "RC300WWmode", true, MAKE_PF_CB(process_RC300WWmode), 21); register_telegram_type(0x2F6, "RC300WW2mode", true, MAKE_PF_CB(process_RC300WWmode), 21); register_telegram_type(0x31B, "RC300WWtemp", true, MAKE_PF_CB(process_RC300WWtemp), 2); @@ -211,12 +219,14 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i } else { register_telegram_type(0x269, "RC300Holiday", true, MAKE_PF_CB(process_RC300Holiday), 18); } + register_telegram_type(0x16E, "Absent", true, MAKE_PF_CB(process_Absent), 1); register_telegram_type(0xBF, "ErrorMessage", false, MAKE_PF_CB(process_ErrorMessageBF)); register_telegram_type(0xC0, "RCErrorMessage", false, MAKE_PF_CB(process_RCErrorMessage2)); if (model == EMSdevice::EMS_DEVICE_FLAG_UI800) { register_telegram_type(0x470, "RC300Summer3", true, MAKE_PF_CB(process_RC300Summer3), 8); } + EMSESP::send_read_request(0xC0, device_id, 0, 20); // read last errorcode on start (only published on errors) // JUNKERS/HT3 @@ -245,6 +255,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i register_telegram_type(set_typeids[i], "JunkersSet", false, MAKE_PF_CB(process_JunkersSet), 20); } } + register_telegram_type(0xBB, "HybridSettings", true, MAKE_PF_CB(process_HybridSettings), 20); register_telegram_type(0x23, "JunkersSetMixer", true, MAKE_PF_CB(process_JunkersSetMixer), 1); register_telegram_type(0x1D3, "JunkersDhw", true, MAKE_PF_CB(process_JunkersWW), 3); @@ -260,6 +271,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i for (uint8_t i = 0; i < monitor_size_final; i++) { EMSESP::send_read_request(monitor_typeids[i], device_id); } + const size_t set_size = set_typeids.size(); for (uint8_t i = 0; i < set_size; i++) { EMSESP::send_read_request(set_typeids[i], device_id); @@ -302,6 +314,7 @@ std::shared_ptr Thermostat::heating_circuit(const st if (telegram->message_length == 0) { return nullptr; } + // look through the Monitor and Set arrays to see if there is a match uint8_t hc_num = 0; // 0 means we haven't found it yet bool toggle_ = false; @@ -472,27 +485,35 @@ std::shared_ptr Thermostat::heating_circuit(const st if (monitor_typeids.size()) { toggle_fetch(monitor_typeids[hc_num - 1], toggle_); } + if (set_typeids.size()) { toggle_fetch(set_typeids[hc_num - 1], toggle_); } + if (hc_num <= set2_typeids.size()) { toggle_fetch(set2_typeids[hc_num - 1], toggle_); } + if (summer_typeids.size()) { toggle_fetch(summer_typeids[hc_num - 1], toggle_); } + if (curve_typeids.size()) { toggle_fetch(curve_typeids[hc_num - 1], toggle_); } + if (timer_typeids.size()) { toggle_fetch(timer_typeids[hc_num - 1], toggle_); } + if (timer2_typeids.size()) { toggle_fetch(timer2_typeids[hc_num - 1], toggle_); } + if (summer2_typeids.size()) { toggle_fetch(summer2_typeids[hc_num - 1], toggle_); } + if (hp_typeids.size()) { toggle_fetch(hp_typeids[hc_num - 1], toggle_); } @@ -636,9 +657,11 @@ std::shared_ptr Thermostat::dhw_circuit(const uint8_t of return dhw_circuit; } } + if (!create) { return nullptr; } + // create a new circuit object and add to the list auto new_dhw = std::make_shared(offset); dhw_circuits_.push_back(new_dhw); @@ -655,6 +678,7 @@ void Thermostat::process_RC10Monitor(const std::shared_ptr & tel if (hc == nullptr) { return; } + uint8_t mode = 1 << hc->mode; telegram->read_value(mode, 0); // 1: nofrost, 2: night, 4: day has_update(hc->mode, mode >> 1); // store as enum 0, 1, 2 @@ -672,6 +696,7 @@ void Thermostat::process_RC10Set(const std::shared_ptr & telegra if (hc == nullptr) { return; } + auto dhw = dhw_circuit(0, true); has_update(telegram, ibaCalIntTemperature_, 0); has_update(telegram, backlight_, 1); @@ -689,6 +714,7 @@ void Thermostat::process_RC10Set_2(const std::shared_ptr & teleg if (hc == nullptr) { return; } + // uint8_t mode = 1 << hc->mode; // telegram->read_value(mode, 0); // 1: nofrost, 2: night, 4: day // has_update(hc->mode, mode >> 1); // store as enum 0, 1, 2 @@ -701,6 +727,7 @@ void Thermostat::process_RC20Set(const std::shared_ptr & telegra if (hc == nullptr) { return; } + has_update(telegram, hc->mode, 23); // 0:off, 1:manual, 2:auto has_update(telegram, hc->nofrosttemp, 24); // for mode off // has_update(telegram, hc->tempautotemp, 28); // no need to read this @@ -714,6 +741,7 @@ void Thermostat::process_RC20Temp(const std::shared_ptr & telegr if (hc == nullptr) { return; } + has_update(telegram, hc->nighttemp, 3); has_update(telegram, hc->daylowtemp, 4); has_update(telegram, hc->daymidtemp, 5); @@ -730,6 +758,7 @@ void Thermostat::process_RC20Timer(const std::shared_ptr & teleg if (hc == nullptr) { return; } + if ((telegram->message_length == 2 && telegram->offset < 83 && !(telegram->offset & 1)) || (!telegram->offset && telegram->message_length > 1 && !strlen(hc->switchtime1))) { char data[sizeof(hc->switchtime1)]; @@ -746,6 +775,7 @@ void Thermostat::process_RC20Timer(const std::shared_ptr & teleg } else { snprintf(data, sizeof(data), "%02d %s %02d:%02d T%d", no, sday, time / 6, 10 * (time % 6), temp); } + has_update(hc->switchtime1, data, sizeof(hc->switchtime1)); } } @@ -758,6 +788,7 @@ void Thermostat::process_RC20Monitor_2(const std::shared_ptr & t if (hc == nullptr) { return; } + // modes byte 0,1: day: 8002, night: 0000, auto-day:0402, auto-night:0400 has_bitupdate(telegram, hc->modetype, 1, 1); // day/night has_update(telegram, hc->selTemp, 2, 1); // is * 2, force as single byte @@ -777,6 +808,7 @@ void Thermostat::process_RC20Set_2(const std::shared_ptr & teleg if (hc == nullptr) { return; } + has_update(telegram, hc->heatingtype, 0); has_update(telegram, hc->nighttemp, 1); // is * 2, has_update(telegram, hc->daytemp, 2); // is * 2, @@ -804,6 +836,7 @@ void Thermostat::process_RemoteTemp(const std::shared_ptr & tele if (telegram->type_id >= 0x435) { return; } + uint8_t hc = telegram->type_id - 0x42B; if (Roomctrl::is_remote(hc)) { toggle_fetch(0x273 + hc, false); @@ -845,6 +878,7 @@ void Thermostat::process_JunkersSet(const std::shared_ptr & tele if (hc == nullptr) { return; } + has_update(telegram, hc->controlmode, 0); // 0-off, 1-unmixed-no-IPM, 2-unmixed-IPM, 3-mixed has_update(telegram, hc->control, 1); // remote: 0-off, 1-FB10, 2-FB100 has_update(telegram, hc->heatingtype, 2); // 0-off, 1-curve, 2-radiator, 3-convector, 4-floor @@ -935,6 +969,7 @@ void Thermostat::process_EasyMonitor(const std::shared_ptr & tel has_update(hc->roomTemp, temp * 10); // * 100 toggle_fetch(0x0A, false); } + int16_t sel = hc->selTemp / 50; if (telegram->read_value(sel, 6, 1)) { // is * 2 has_update(hc->selTemp, sel * 50); // * 100 @@ -1001,11 +1036,13 @@ void Thermostat::process_RC35wwTimer(const std::shared_ptr & tel } else { snprintf(data, sizeof(data), "%02d %s %02d:%02d %s", no, sday, time / 6, 10 * (time % 6), on ? "on" : "off"); } + if (telegram->type_id == 0x38) { has_update(dhw->wwSwitchTime_, data, sizeof(dhw->wwSwitchTime_)); } else { has_update(dhw->wwCircSwitchTime_, data, sizeof(dhw->wwCircSwitchTime_)); } + if (is_fetch(telegram->type_id)) { toggle_fetch(telegram->type_id, false); // dont fetch again } @@ -1098,6 +1135,7 @@ void Thermostat::process_JunkersSetMixer(const std::shared_ptr & if (hc == nullptr) { return; } + has_update(telegram, hc->targetflowtemp, 0); } @@ -1190,6 +1228,7 @@ void Thermostat::process_RC300Monitor(const std::shared_ptr & te has_update(hc->summermode, EMS_VALUE_UINT8_NOTSET); } } + has_update(telegram, hc->targetflowtemp, 4); has_update(telegram, hc->curroominfl, 27); has_update(telegram, hc->currSolarInfl, 29); @@ -1230,6 +1269,7 @@ void Thermostat::process_RC300Set(const std::shared_ptr & telegr if ((uint8_t)tat == 0xFF) { tat = -2; } + has_update(hc->tempautotemp, tat); if (model() == EMS_DEVICE_FLAG_CR120) { @@ -1237,6 +1277,7 @@ void Thermostat::process_RC300Set(const std::shared_ptr & telegr } else { has_update(telegram, hc->manualtemp, 10); // is * 2 } + has_enumupdate(telegram, hc->program, 11, 1); // timer program 1 or 2 has_enumupdate(telegram, hc->reducemode, 5, 1); // 1-outdoor temp threshold, 2-room temp threshold, 3-reduced mode @@ -1272,6 +1313,7 @@ void Thermostat::process_RC300Summer(const std::shared_ptr & tel } else { has_update(telegram, hc->designtemp, 5); } + // minflowtemp could be in 8 or 13 #2969 has_update(telegram, hc->minflowtemp, 13); has_update(telegram, hc->baseflowtemp, 8); @@ -1292,11 +1334,13 @@ bool Thermostat::has_hpoperatingstate() const { if (has_summer3()) { return true; } + for (const uint16_t type_id : summer2_typeids) { if (is_received(type_id)) { return true; } } + return false; } @@ -1320,6 +1364,7 @@ void Thermostat::process_RC300Summer2(const std::shared_ptr & te if (hc == nullptr) { return; } + if (hc->statusbyte & 1) { has_update(telegram, hc->summersetmode, 0); has_update(hc->hpoperatingmode, EMS_VALUE_UINT8_NOTSET); @@ -1327,6 +1372,7 @@ void Thermostat::process_RC300Summer2(const std::shared_ptr & te has_update(telegram, hc->hpoperatingmode, 0); has_update(hc->summersetmode, EMS_VALUE_UINT8_NOTSET); } + has_update(telegram, hc->summertemp, 1); has_update(telegram, hc->heatondelay, 2); has_update(telegram, hc->heatoffdelay, 3); @@ -1374,6 +1420,7 @@ void Thermostat::process_RC300WWmode(const std::shared_ptr & tel if (dhw == nullptr) { return; } + // circulation pump see: https://github.com/Th3M3/buderus_ems-wiki/blob/master/Einstellungen%20der%20Bedieneinheit%20RC310.md has_update(telegram, dhw->wwCircPump_, 1); // FF=off, 0=on ? @@ -1389,6 +1436,7 @@ void Thermostat::process_RC300WWmode(const std::shared_ptr & tel } else { has_update(telegram, dhw->wwMode_, 2); // 0=off, 1=low, 2=high, 3=auto, 4=own prog } + has_update(telegram, dhw->wwCircMode_, 3); // 0=off, 1=on, 2=auto, 4=own? has_update(telegram, dhw->wwChargeDuration_, 10); // value in steps of 15 min has_update(telegram, dhw->wwCharge_, 11); // 0=off, 0xFF=on @@ -1406,6 +1454,7 @@ void Thermostat::process_RC300WWmode2(const std::shared_ptr & te if (dhw == nullptr) { return; } + // 0x31D for WW system 1, 0x31E for WW system 2 // pos 1 = holiday mode // pos 2 = current status of DHW setpoint @@ -1439,6 +1488,7 @@ void Thermostat::process_RC300Set2(const std::shared_ptr & teleg if (hc == nullptr) { return; } + has_update(telegram, hc->wwprio, 3); } @@ -1454,6 +1504,7 @@ void Thermostat::process_RC300Holiday(const std::shared_ptr & te if (telegram->offset || telegram->message_length < 6) { return; } + char data[sizeof(vacation[0]) + 4]; snprintf(data, sizeof(data), @@ -1473,6 +1524,7 @@ void Thermostat::process_PID(const std::shared_ptr & telegram) { if (hc == nullptr) { return; } + // Offset 0: Floorheating integral time in 10 min steps. Default is 25, range 16 to 32 // Offset 1: HC radiator/convector intergral time in 1min steps. Default is 16, range 3 to 80 // Offset 2: Heat Control characteristic as you figured out. Default is 3, Range 2-4 @@ -1482,6 +1534,7 @@ void Thermostat::process_PID(const std::shared_ptr & telegram) { } else if (hc->heatingtype != EMS_VALUE_UINT8_NOTSET) { has_update(telegram, hc->inttime, 1); } + has_enumupdate(telegram, hc->heatingpid, 2, 2); has_update(telegram, hc->pumpopt, 3); } @@ -1493,6 +1546,7 @@ void Thermostat::process_HPMode(const std::shared_ptr & telegram if (hc == nullptr) { return; } + has_update(telegram, hc->control, 3); has_update(telegram, hc->hpmode, 5); } @@ -1503,11 +1557,13 @@ void Thermostat::process_HPSet(const std::shared_ptr & telegram) if (hc == nullptr) { return; } + if (hc->control == 3 || hc->control == 6) { // control with humidity has_update(telegram, hc->hpminflowtemp, 0); // 7-35°C } else { has_update(telegram, hc->hpminflowtemp, 1); // 7-35°C } + // has_update(telegram, hc->roomtempoffset, 2); // 1-10K // not implemented, mentioned in #3144 has_update(telegram, hc->roomtempdiff, 3); // 1-10K has_update(telegram, hc->dewoffset, 4); // 2-10K @@ -1535,6 +1591,7 @@ void Thermostat::process_RC30Set(const std::shared_ptr & telegra if (hc == nullptr) { return; } + has_update(telegram, ibaLanguage_, 0); // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian has_update(telegram, ibaCalIntTemperature_, 1); // offset int. temperature sensor, by * 0.1 Kelvin has_update(telegram, autodst_, 2); // Automatic change Daylight Saving time: (0x00 = off, 0xFF = on) @@ -1644,6 +1701,7 @@ void Thermostat::process_RC35Set(const std::shared_ptr & telegra has_update(telegram, hc->designtemp, 17); // is * 1 has_update(telegram, hc->maxflowtemp, 15); // is * 1 } + setValueEnum(&hc->heatingtype, hc->control == 1 ? FL_(enum_heatingtype2) : FL_(enum_heatingtype)); } @@ -1673,6 +1731,7 @@ void Thermostat::process_RC35Timer(const std::shared_ptr & teleg } else { snprintf(data, sizeof(data), "%02d %s %02d:%02d %s", no, sday, time / 6, 10 * (time % 6), on ? "on" : "off"); } + if (!prog) { strlcpy(hc->switchtime1, data, sizeof(hc->switchtime1)); has_update(hc->switchtime1); // always publish @@ -1731,6 +1790,7 @@ void Thermostat::process_RC30Vacation(const std::shared_ptr & te if ((telegram->offset + telegram->message_length) > 57) { return; } + static uint8_t vacation_telegram[57] = {0}; // make a copy of the whole telegram to access blocks memcpy(&vacation_telegram[telegram->offset], telegram->message_data, telegram->message_length); for (uint8_t index = 0, pos = 0; index < 8; index++, pos += 7) { @@ -1770,8 +1830,10 @@ void Thermostat::process_RCTime(const std::shared_ptr & telegram } else { set_datetime("ntp", -1); // set from NTP } + setTimeRetry++; } + return; } @@ -1825,6 +1887,7 @@ void Thermostat::process_RCTime(const std::shared_ptr & telegram set_datetime("ntp", -1); // set from NTP LOG_INFO("thermostat time correction %d seconds from ntp", difference); } + setTimeRetry++; } } else { @@ -1905,6 +1968,7 @@ void Thermostat::process_RCErrorMessage2(const std::shared_ptr & if (telegram->offset > 0 || telegram->message_length < 20) { return; } + uint8_t code[4] = {telegram->message_data[5], telegram->message_data[6], telegram->message_data[7], 0}; uint16_t codeNo = telegram->message_data[8] * 256 + telegram->message_data[9]; uint16_t year = (telegram->message_data[10] & 0x7F) + 2000; @@ -1940,6 +2004,7 @@ void Thermostat::process_RCErrorMessage2(const std::shared_ptr & hour1, min1); } + has_update(lastCode_); } } @@ -1956,23 +2021,28 @@ bool Thermostat::set_roomtempdiff(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int v; if (Helpers::value2number(value, v)) { write_command(hp_typeids[hc->hc()], 3, v, hp_typeids[hc->hc()]); return true; } + return false; } + bool Thermostat::set_dewoffset(const char * value, const int8_t id) { auto hc = heating_circuit(id); if (hc == nullptr) { return false; } + int v; if (Helpers::value2number(value, v)) { write_command(hp_typeids[hc->hc()], 4, v, hp_typeids[hc->hc()]); return true; } + return false; } @@ -1981,6 +2051,7 @@ bool Thermostat::set_hpminflowtemp(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int v; if (Helpers::value2temperature(value, v)) { if (hc->control == 3 || hc->control == 6) { // remotes with humidity @@ -1988,8 +2059,10 @@ bool Thermostat::set_hpminflowtemp(const char * value, const int8_t id) { } else { write_command(hp_typeids[hc->hc()], 1, v, hp_typeids[hc->hc()]); } + return true; } + return false; } @@ -1998,10 +2071,12 @@ bool Thermostat::set_hpmode(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + uint8_t v; if (!Helpers::value2enum(value, v, FL_(enum_hpmode))) { return false; } + write_command(hpmode_typeids[hc->hc()], 5, v, hpmode_typeids[hc->hc()]); return true; } @@ -2012,6 +2087,7 @@ bool Thermostat::set_hybridStrategy(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_hybridStrategy))) { return false; } + write_command(0xBB, 12, v + 1, 0xBB); return true; } @@ -2021,6 +2097,7 @@ bool Thermostat::set_switchOverTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + write_command(0xBB, 13, v, 0xBB); return true; } @@ -2030,6 +2107,7 @@ bool Thermostat::set_energyCostRatio(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + write_command(0xBB, 14, (uint8_t)(v * 10), 0xBB); return true; } @@ -2039,6 +2117,7 @@ bool Thermostat::set_fossileFactor(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + write_command(0xBB, 15, (uint8_t)(v * 10), 0xBB); return true; } @@ -2048,6 +2127,7 @@ bool Thermostat::set_electricFactor(const char * value, const int8_t id) { if (!Helpers::value2float(value, v)) { return false; } + write_command(0xBB, 16, (uint8_t)(v * 10), 0xBB); return true; } @@ -2057,6 +2137,7 @@ bool Thermostat::set_delayBoiler(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + write_command(0xBB, 18, v, 0xBB); return true; } @@ -2066,6 +2147,7 @@ bool Thermostat::set_tempDiffBoiler(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v, true)) { return false; } + write_command(0xBB, 19, v, 0xBB); return true; } @@ -2076,6 +2158,7 @@ bool Thermostat::set_pvEnableWw(const char * value, const int8_t id) { write_command(0x23E, 3, v ? 0xFF : 0, 0x23E); return true; } + return false; } @@ -2085,6 +2168,7 @@ bool Thermostat::set_pvRaiseHeat(const char * value, const int8_t id) { write_command(0x23E, 0, v, 0x23E); return true; } + return false; } @@ -2094,6 +2178,7 @@ bool Thermostat::set_pvLowerCool(const char * value, const int8_t id) { write_command(0x23E, 5, v, 0x23E); return true; } + return false; } @@ -2176,6 +2261,7 @@ bool Thermostat::set_display(const char * value, const int8_t id) { return true; } } + return false; } @@ -2262,6 +2348,7 @@ bool Thermostat::set_remotehum(const char * value, const int8_t id) { Roomctrl::set_remotehum(Roomctrl::RT800, hc->hc(), hc->remotehum); return true; } + Roomctrl::set_remotehum(Roomctrl::RC100H, hc->hc(), EMS_VALUE_UINT8_NOTSET); // RC100H return false; } @@ -2274,8 +2361,10 @@ bool Thermostat::set_building(const char * value, const int8_t id) { write_command(0x110, 17, i / 10, 0x110); return true; } + return false; } + uint8_t bd; if (!Helpers::value2enum(value, bd, FL_(enum_ibaBuildingType))) { return false; @@ -2310,6 +2399,7 @@ bool Thermostat::set_heatingpid(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + write_command(set2_typeids[hc->hc()], 2, pid + 2, set2_typeids[hc->hc()]); } @@ -2321,11 +2411,13 @@ bool Thermostat::set_pumpopt(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + bool b; if (Helpers::value2bool(value, b)) { write_command(set2_typeids[hc->hc()], 3, b ? 0xFF : 0, set2_typeids[hc->hc()]); return true; } + return false; } @@ -2334,11 +2426,13 @@ bool Thermostat::set_inttime(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int v; if (Helpers::value2number(value, v)) { write_command(set2_typeids[hc->hc()], 1, v, set2_typeids[hc->hc()]); return true; } + return false; } @@ -2347,11 +2441,13 @@ bool Thermostat::set_inttimefloor(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int v; if (Helpers::value2number(value, v)) { write_command(set2_typeids[hc->hc()], 0, v / 10, set2_typeids[hc->hc()]); return true; } + return false; } @@ -2366,8 +2462,10 @@ bool Thermostat::set_damping(const char * value, const int8_t id) { } else { write_command(EMS_TYPE_IBASettings, 21, dmp ? 0xFF : 0, EMS_TYPE_IBASettings); } + return true; } + return false; } @@ -2380,8 +2478,10 @@ bool Thermostat::set_solar(const char * value, const int8_t) { } else { write_command(0x240, 13, b ? 0xFF : 0, 0x240); } + return true; } + return false; } @@ -2393,16 +2493,19 @@ bool Thermostat::set_language(const char * value, const int8_t id) { if (!Helpers::value2enum(value, lg, FL_(enum_ibaLanguageJ))) { return false; } + write_command(0x11E, 3, lg, 0x11E); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_RC30) { if (!Helpers::value2enum(value, lg, FL_(enum_ibaLanguage_RC30))) { return false; } + write_command(EMS_TYPE_RC30Settings, 0, lg, EMS_TYPE_RC30Settings); } else { if (!Helpers::value2enum(value, lg, FL_(enum_ibaLanguage))) { return false; } + write_command(EMS_TYPE_IBASettings, 1, lg, EMS_TYPE_IBASettings); } @@ -2425,6 +2528,7 @@ bool Thermostat::set_control(const char * value, const int8_t id) { write_command(set_typeids[hc->hc()], 1, ctrl); return true; } + // BC400 // 1-RC100, 2-RC100H, 3-RC200 } else if (model() == EMSdevice::EMS_DEVICE_FLAG_BC400) { @@ -2445,6 +2549,7 @@ bool Thermostat::set_control(const char * value, const int8_t id) { Roomctrl::set_remotetemp(0, hc->hc(), hc->remotetemp); } } + return true; } } else if (model() == EMSdevice::EMS_DEVICE_FLAG_UI800) { @@ -2459,8 +2564,10 @@ bool Thermostat::set_control(const char * value, const int8_t id) { Roomctrl::set_remotetemp(0, hc->hc(), hc->remotetemp); } } + return true; } + // RC100 or RC300 // 1-RC200, 2-RC100, 3-RC100H } else if (isRC300() || model() == EMSdevice::EMS_DEVICE_FLAG_RC100) { @@ -2478,8 +2585,10 @@ bool Thermostat::set_control(const char * value, const int8_t id) { hc->remotetemp = EMS_VALUE_INT16_NOTSET; Roomctrl::set_remotetemp(0, hc->hc(), hc->remotetemp); } + // Roomctrl::set_remotetemp(ctrl == 1 ? Roomctrl::RC200 : ctrl == 3 ? Roomctrl::RC100H : Roomctrl::RC100, hc->hc(), hc->remotetemp); } + return true; } } else if (Helpers::value2enum(value, ctrl, FL_(enum_control))) { @@ -2505,6 +2614,7 @@ bool Thermostat::set_roomsensor(const char * value, const int8_t id) { return true; } } + return false; } @@ -2514,47 +2624,56 @@ bool Thermostat::set_wwmode(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + uint8_t set; if (model() == EMSdevice::EMS_DEVICE_FLAG_RC10) { if (!Helpers::value2enum(value, set, FL_(enum_wwMode3))) { return false; } + write_command(0xB0, 2, set, 0xB0); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_BC400 || model() == EMSdevice::EMS_DEVICE_FLAG_UI800 || model() == EMSdevice::EMS_DEVICE_FLAG_HMC310) { if (!Helpers::value2enum(value, set, FL_(enum_wwMode4), {0, 5, 1, 2, 4})) { // off, eco+, eco, comfort, auto return false; } + write_command(0x02F5 + dhw->offset(), 2, set, 0x02F5 + dhw->offset()); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_CR120) { if (!Helpers::value2enum(value, set, FL_(enum_wwMode6), {0, 2, 4})) { // normal, comfort, auto return false; } + write_command(0x02F5 + dhw->offset(), 2, set, 0x02F5 + dhw->offset()); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_RC100) { if (!Helpers::value2enum(value, set, FL_(enum_wwMode2), {0, 2, 3})) { // off, on, auto return false; } + write_command(0x02F5 + dhw->offset(), 2, set, 0x02F5 + dhw->offset()); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_R3000) { // Rego3000 - https://github.com/emsesp/EMS-ESP32/issues/1692 if (!Helpers::value2enum(value, set, FL_(enum_wwMode5), {1, 2, 5})) { return false; } + write_command(0x02F5 + dhw->offset(), 2, set, 0x02F5 + dhw->offset()); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_RC300) { if (!Helpers::value2enum(value, set, FL_(enum_wwMode))) { return false; } + write_command(0x02F5 + dhw->offset(), 2, set, 0x02F5 + dhw->offset()); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_RC30) { if (!Helpers::value2enum(value, set, FL_(enum_wwMode3))) { return false; } + write_command(EMS_TYPE_RC30wwSettings, 0, set, EMS_TYPE_RC30wwSettings); } else { if (!Helpers::value2enum(value, set, FL_(enum_wwMode2))) { return false; } + write_command(EMS_TYPE_wwSettings, 2, set, EMS_TYPE_wwSettings); } @@ -2603,6 +2722,7 @@ bool Thermostat::set_wwcharge(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + bool b; if (!Helpers::value2bool(value, b)) { return false; @@ -2629,10 +2749,12 @@ bool Thermostat::set_wwchargeduration(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + int t; if (!Helpers::value2number(value, t)) { return false; } + t = (t + 8) / 15; write_command(0x2F5 + dhw->offset(), 10, t, 0x02F5 + dhw->offset()); @@ -2650,6 +2772,7 @@ bool Thermostat::set_wwprio(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (isRC300()) { write_command(set2_typeids[hc->hc()], 3, b ? 0xFF : 0x00, set2_typeids[hc->hc()]); } else { @@ -2670,6 +2793,7 @@ bool Thermostat::set_cooling(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(set_typeids[hc->hc()], 28, b ? 0x01 : 0x00, set_typeids[hc->hc()]); return true; } @@ -2680,14 +2804,17 @@ bool Thermostat::set_coolondelay(const char * value, const int8_t id) { if (!Helpers::value2float(value, f)) { return false; } + if (has_summer3()) { write_command(0x470, 6, (uint8_t)(f * 4), 0x470); return true; } + auto hc = heating_circuit(id); if (hc == nullptr) { return false; } + write_command(summer2_typeids[hc->hc()], 6, (uint8_t)f, summer2_typeids[hc->hc()]); return true; } @@ -2697,14 +2824,17 @@ bool Thermostat::set_cooloffdelay(const char * value, const int8_t id) { if (!Helpers::value2float(value, f)) { return false; } + if (has_summer3()) { write_command(0x470, 7, (uint8_t)(f * 4), 0x470); return true; } + auto hc = heating_circuit(id); if (hc == nullptr) { return false; } + write_command(summer2_typeids[hc->hc()], 7, (uint8_t)f, summer2_typeids[hc->hc()]); return true; } @@ -2714,10 +2844,12 @@ bool Thermostat::set_switchProgMode(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + uint8_t set; if (!Helpers::value2enum(value, set, FL_(enum_switchProgMode))) { return false; } + write_command(set_typeids[hc->hc()], 19, set + 1, set_typeids[hc->hc()]); return true; } @@ -2728,15 +2860,18 @@ bool Thermostat::set_wwcircmode(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + uint8_t set; if (isRC300() || (model() == EMSdevice::EMS_DEVICE_FLAG_RC100)) { if (!Helpers::value2enum(value, set, FL_(enum_wwCircMode))) { return false; } + write_command(0x02F5 + dhw->offset(), 3, set, 0x02F5 + dhw->offset()); return true; } + if (!Helpers::value2enum(value, set, FL_(enum_wwMode2))) { return false; } @@ -2751,6 +2886,7 @@ bool Thermostat::set_wwDailyHeating(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + bool b; if (!Helpers::value2bool(value, b)) { return false; @@ -2765,6 +2901,7 @@ bool Thermostat::set_wwDailyHeatTime(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + int set; if (!Helpers::value2number(value, set)) { return false; @@ -2778,6 +2915,7 @@ bool Thermostat::set_wwDailyHeatTime(const char * value, const int8_t id) { write_command(0x2F5 + dhw->offset(), 9, t, 0x2F5 + dhw->offset()); } + return true; } @@ -2786,6 +2924,7 @@ bool Thermostat::set_wwDisinfect(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + bool b; if (!Helpers::value2bool(value, b)) { return false; @@ -2807,6 +2946,7 @@ bool Thermostat::set_wwDisinfectDay(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + uint8_t set; if (!Helpers::value2enum(value, set, FL_(enum_dayOfWeek))) { return false; @@ -2828,21 +2968,25 @@ bool Thermostat::set_wwDisinfectHour(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + int set; if (isRC300() || (model() == EMSdevice::EMS_DEVICE_FLAG_RC100)) { if (!Helpers::value2number(value, set, 0, 1431)) { return false; } + write_command(0x2F5 + dhw->offset(), 6, (set + 8) / 15, 0x2F5 + dhw->offset()); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_RC30) { if (!Helpers::value2number(value, set, 0, 23)) { return false; } + write_command(EMS_TYPE_RC30wwSettings, 4, set, EMS_TYPE_RC30wwSettings); } else { if (!Helpers::value2number(value, set, 0, 23)) { return false; } + write_command(0x37, 6, set, 0x37); } @@ -2965,6 +3109,7 @@ bool Thermostat::set_holiday(const char * value, const int8_t id, const bool vac if (!data[0] || data[0] > 31 || !data[1] || data[1] > 12 || !data[3] || data[3] > 31 || !data[4] || data[4] > 12) { return false; } + if (!vacation || value[10] == '+') { // + for compatibility if (hc) { write_command(timer_typeids[hc->hc()], 93, data, 6, timer_typeids[hc->hc()]); @@ -3003,10 +3148,12 @@ bool Thermostat::set_RC30Vacation(const char * value, const int8_t id) { if (!data[0] || data[0] > 31 || !data[1] || data[1] > 12 || !data[3] || data[3] > 31 || !data[4] || data[4] > 12) { return false; } + if (model() == EMSdevice::EMS_DEVICE_FLAG_R3000) { write_command(0x269, 0, data, 6, 0x269); return true; } + // RC30 write_command(0xA9, 1 + 7 * (id - 1), data, 6, 0x9A); return true; @@ -3030,11 +3177,13 @@ bool Thermostat::set_R3000Holiday(const char * value, const int8_t id) { if (!data[2] || data[2] > 31 || !data[1] || data[1] > 12 || !data[5] || data[5] > 31 || !data[4] || data[4] > 12) { return false; } + if (model() == EMSdevice::EMS_DEVICE_FLAG_R3000) { write_command(0x269, 0, data, 6, 0x269); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_RC100) { write_command(0x43F, 0, data, 6, 0x43F); } + return true; } @@ -3044,10 +3193,12 @@ bool Thermostat::set_pause(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + if (value == nullptr) { read_command(timer_typeids[hc->hc()], 85, 1); return true; } + int hrs = 0; if (!Helpers::value2number(value, hrs)) { return false; @@ -3087,6 +3238,7 @@ bool Thermostat::set_absent(const char * value, const int8_t) { write_command(0x16E, 0, b ? 0xFF : 0, 0x16E); return true; } + return false; } @@ -3095,11 +3247,13 @@ bool Thermostat::set_redthreshold(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + float t; if (Helpers::value2temperature(value, t)) { write_command(set_typeids[hc->hc()], 20, (int8_t)(t * 2), set_typeids[hc->hc()]); return true; } + return false; } @@ -3108,11 +3262,13 @@ bool Thermostat::set_solarinfl(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + float t; if (Helpers::value2temperature(value, t)) { write_command(summer_typeids[hc->hc()], 3, (int8_t)t, summer_typeids[hc->hc()]); return true; } + return false; } @@ -3141,6 +3297,7 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { if (tm_->tm_year < 110) { // no valid time return false; } + if (!EMSESP::system_.ntp_connected()) { LOG_WARNING("Set date: no valid NTP data, setting from ESP Clock"); } @@ -3194,6 +3351,7 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { LOG_WARNING("Set date: invalid data, wrong length"); return false; } + if (data[1] == 0 || data[1] > 12 || data[2] > 23 || data[3] == 0 || data[3] > 31 || data[4] > 59 || data[5] > 59 || data[6] > 6 || data[7] > 3) { LOG_WARNING("Invalid date/time: %02d.%02d.2%03d-%02d:%02d:%02d-%d-%d", data[3], data[1], data[0], data[2], data[4], data[5], data[6], data[7]); return false; @@ -3216,10 +3374,12 @@ bool Thermostat::set_roominfl_factor(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + float val; if (!Helpers::value2float(value, val)) { return false; } + if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { write_command(set_typeids[hc->hc()], 6, (uint8_t)((val + 5) / 10)); @@ -3236,10 +3396,12 @@ bool Thermostat::set_roominfl_mode(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + uint8_t val; if (!Helpers::value2enum(value, val, FL_(enum_roominfluence))) { return false; } + write_command(set_typeids[hc->hc()], 7, (uint8_t)val); return true; @@ -3307,30 +3469,39 @@ bool Thermostat::set_mode(const char * value, const int8_t id) { if (!strcmp(mode, FL_(auto)[0])) { return set_mode_n(HeatingCircuit::Mode::AUTO, id); } + if (!strcmp(mode, FL_(off)[0])) { return set_mode_n(HeatingCircuit::Mode::OFF, id); } + if (!strcmp(mode, FL_(manual)[0])) { return set_mode_n(HeatingCircuit::Mode::MANUAL, id); } + if (!strcmp(mode, FL_(day)[0])) { return set_mode_n(HeatingCircuit::Mode::DAY, id); } + if (!strcmp(mode, FL_(night)[0])) { return set_mode_n(HeatingCircuit::Mode::NIGHT, id); } + if (!strcmp(mode, FL_(heat)[0])) { return set_mode_n(HeatingCircuit::Mode::HEAT, id); } + if (!strcmp(mode, FL_(nofrost)[0])) { return set_mode_n(HeatingCircuit::Mode::NOFROST, id); } + if (!strcmp(mode, FL_(eco)[0])) { return set_mode_n(HeatingCircuit::Mode::ECO, id); } + if (!strcmp(mode, FL_(holiday)[0])) { return set_mode_n(HeatingCircuit::Mode::HOLIDAY, id); } + if (!strcmp(mode, FL_(comfort)[0])) { return set_mode_n(HeatingCircuit::Mode::COMFORT, id); } @@ -3376,8 +3547,10 @@ bool Thermostat::set_mode_n(const uint8_t mode, const int8_t id) { if (hc->selTemp > 0) { return true; } + set_mode_value = 50; // set selTemp to 5 degrees } + break; case EMSdevice::EMS_DEVICE_FLAG_RC10: offset = 0; @@ -3390,6 +3563,7 @@ bool Thermostat::set_mode_n(const uint8_t mode, const int8_t id) { } else { // DAY set_mode_value = 4; } + break; case EMSdevice::EMS_DEVICE_FLAG_RC20: offset = EMS_OFFSET_RC20Set_mode; @@ -3423,6 +3597,7 @@ bool Thermostat::set_mode_n(const uint8_t mode, const int8_t id) { } else { offset = EMS_OFFSET_JunkersSetMessage_set_mode; } + // validate_typeid = monitor_typeids[hc_p]; if (mode == HeatingCircuit::Mode::NOFROST) { set_mode_value = 0x01; @@ -3433,6 +3608,7 @@ bool Thermostat::set_mode_n(const uint8_t mode, const int8_t id) { } else if (mode == HeatingCircuit::Mode::AUTO) { set_mode_value = 0x04; } + break; default: offset = 0; @@ -3456,6 +3632,7 @@ bool Thermostat::set_mode_n(const uint8_t mode, const int8_t id) { } else { hc->mode = set_mode_value; } + has_update(&hc->mode); return true; @@ -3469,8 +3646,10 @@ bool Thermostat::set_summermode(const char * value, const int8_t id) { write_command(0x470, 0, set, 0x470); return true; } + return false; } + auto hc = heating_circuit(id); if (hc == nullptr) { return false; @@ -3515,10 +3694,12 @@ bool Thermostat::set_comfortPointTemp(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int set; if (!Helpers::value2temperature(value, set)) { return false; } + write_command(summer_typeids[hc->hc()], 12, set, summer_typeids[hc->hc()]); return true; } @@ -3528,10 +3709,12 @@ bool Thermostat::set_comfortPointOffset(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int set; if (!Helpers::value2temperature(value, set, true)) { return false; } + write_command(summer_typeids[hc->hc()], 11, set, summer_typeids[hc->hc()]); return true; } @@ -3548,11 +3731,13 @@ bool Thermostat::set_holidaymode(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_mode4))) { return false; } + if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS && has_flags(EMSdevice::EMS_DEVICE_FLAG_JUNKERS_OLD)) { write_command(set_typeids[hc->hc()], 8, v + 1, set_typeids[hc->hc()]); } else { write_command(set_typeids[hc->hc()], 19, v + 1, set_typeids[hc->hc()]); } + return true; } @@ -3568,6 +3753,7 @@ bool Thermostat::set_heatup(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_heatup))) { return false; } + write_command(set_typeids[hc->hc()], 18, v + 1, set_typeids[hc->hc()]); return true; } @@ -3584,6 +3770,7 @@ bool Thermostat::set_switchonoptimization(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (model() == EMSdevice::EMS_DEVICE_FLAG_RC35) { write_command(set_typeids[hc->hc()], 19, b ? 0xFF : 0x00, set_typeids[hc->hc()]); } else if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS && has_flags(EMSdevice::EMS_DEVICE_FLAG_JUNKERS_OLD)) { @@ -3591,6 +3778,7 @@ bool Thermostat::set_switchonoptimization(const char * value, const int8_t id) { } else { write_command(curve_typeids[hc->hc()], 4, b ? 0xFF : 0x00, curve_typeids[hc->hc()]); } + return true; } @@ -3599,10 +3787,12 @@ bool Thermostat::set_boost(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + bool b; if (!Helpers::value2bool(value, b)) { return false; } + write_command(set_typeids[hc->hc()], 23, b ? 0xFF : 0x00, set_typeids[hc->hc()]); return true; } @@ -3612,10 +3802,12 @@ bool Thermostat::set_boosttime(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + int v; if (!Helpers::value2number(value, v)) { return false; } + write_command(set_typeids[hc->hc()], 24, (uint8_t)v, set_typeids[hc->hc()]); return true; } @@ -3625,14 +3817,17 @@ bool Thermostat::set_heatondelay(const char * value, const int8_t id) { if (!Helpers::value2float(value, f)) { return false; } + if (has_summer3()) { write_command(0x470, 2, (uint8_t)(f * 4), 0x470); return true; } + auto hc = heating_circuit(id); if (hc == nullptr) { return false; } + write_command(summer2_typeids[hc->hc()], 2, (uint8_t)f, summer2_typeids[hc->hc()]); return true; } @@ -3642,14 +3837,17 @@ bool Thermostat::set_heatoffdelay(const char * value, const int8_t id) { if (!Helpers::value2float(value, f)) { return false; } + if (has_summer3()) { write_command(0x470, 3, (uint8_t)(f * 4), 0x470); return true; } + auto hc = heating_circuit(id); if (hc == nullptr) { return false; } + write_command(summer2_typeids[hc->hc()], 3, (uint8_t)f, summer2_typeids[hc->hc()]); return true; } @@ -3659,14 +3857,17 @@ bool Thermostat::set_instantstart(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + if (has_summer3()) { write_command(0x470, 4, (uint8_t)v, 0x470); return true; } + auto hc = heating_circuit(id); if (hc == nullptr) { return false; } + write_command(summer2_typeids[hc->hc()], 4, (uint8_t)v, summer2_typeids[hc->hc()]); return true; } @@ -3690,6 +3891,7 @@ bool Thermostat::set_reducemode(const char * value, const int8_t id) { return true; } } + return false; } @@ -3715,6 +3917,7 @@ bool Thermostat::set_nofrostmode(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + uint8_t set; if (isRC300() || model() == EMSdevice::EMS_DEVICE_FLAG_RC100) { if (Helpers::value2enum(value, set, FL_(enum_nofrostmode1))) { @@ -3727,6 +3930,7 @@ bool Thermostat::set_nofrostmode(const char * value, const int8_t id) { return true; } } + return false; } @@ -3756,6 +3960,7 @@ bool Thermostat::set_heatingtype(const char * value, const int8_t id) { } else { write_command(curve_typeids[hc->hc()], 1, set, curve_typeids[hc->hc()]); } + return true; } @@ -3822,6 +4027,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char if (value == nullptr) { return false; } + uint8_t no = 44; // invalid value uint8_t day = 8; // invalid uint8_t on = 8; // invalid @@ -3833,19 +4039,23 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char if (error) { return false; } + if (!doc["no"].is()) { return false; } + no = doc["no"]; if (!doc["day"].is() || !doc["mode"].is() || !doc["time"].is()) { EMSESP::wait_validate(type_id); read_command(type_id, 2 * no, 2); return true; } + const char * s_day = doc["day"]; if (!Helpers::value2enum(s_day, day, FL_(enum_dayOfWeek))) { return false; } + const char * s_mode = doc["mode"]; const char * s_time = doc["time"]; if (model() == EMSdevice::EMS_DEVICE_FLAG_RC35 || model() == EMSdevice::EMS_DEVICE_FLAG_RC30_N) { @@ -3853,6 +4063,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char if (Helpers::value2bool(s_mode, b)) { on = b ? 1 : 0; } + if (strlen(s_time) == 5 && s_time[2] == ':') { time = 6 * ((s_time[0] - '0') * 10 + (s_time[1] - '0')) + (s_time[3] - '0'); } @@ -3862,6 +4073,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char } else { on = s_mode[0] - '0'; } + if (strlen(s_time) == 5 && s_time[2] == ':') { time = 6 * ((s_time[0] - '0') * 10 + (s_time[1] - '0')) + (s_time[3] - '0'); } @@ -3871,6 +4083,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char time = 4 * ((s_time[0] - '0') * 10 + (s_time[1] - '0')) + ((s_time[3] - '0') * 10 + (s_time[4] - '0')) / 15; } } + if (strncmp(s_mode, "not_set", 7) == 0) { day = 7; on = 7; @@ -3881,14 +4094,17 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char if (strlen(value) > 1) { no = (value[0] - '0') * 10 + (value[1] - '0'); } + if (strlen(value) == 2) { if (no < 42) { EMSESP::wait_validate(type_id); read_command(type_id, 2 * no, 2); return true; } + return false; } + if (strlen(value) > 4) { for (uint8_t i = 0; i < 7; i++) { // we use EN settings for the day abbreviation @@ -3897,9 +4113,11 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char } } } + if (strlen(value) > 10) { time = 6 * ((value[6] - '0') * 10 + (value[7] - '0')) + (value[9] - '0'); } + if (strlen(value) > 13 && value[12] == 'o') { on = value[13] == 'n' ? 1 : 0; } else if (strlen(value) == 14 && value[12] == 'T') { @@ -3907,6 +4125,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char } else if (strlen(value) == 13) { on = value[12] - '0'; } + if (strlen(value) >= 10 && strncmp(&value[3], "not_set", 7) == 0) { day = 7; on = 7; @@ -3914,6 +4133,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char // LOG_INFO("switchtime %02d cleared", no); } } + uint8_t data[2] = {0xE7, 0x90}; // unset switchtime if (day != 7 && on != 7) { data[0] = (day << 5) + on; @@ -3926,11 +4146,13 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char min_on = 0; max_on = 1; } + if (no > 41 || time > 0x90 || day > 7 || ((on < min_on || on > max_on) && on != 7)) { // LOG_WARNING("Setting switchtime: Invalid data: %s", value); // LOG_WARNING("Setting switchtime: Invalid data: %02d.%1d.0x%02X.%1d", no, day, time, on); return false; } + if (data[0] != 0xE7) { // we use EN settings for the day abbreviation auto sday = (FL_(enum_dayOfWeek)[day][0]); @@ -3945,6 +4167,7 @@ bool Thermostat::set_switchtime(const char * value, const uint16_t type_id, char } else { snprintf(out, len, "%02d not_set", no); } + write_command(type_id, no * 2, &data[0], 2, 0); return true; } @@ -3955,14 +4178,17 @@ bool Thermostat::set_switchtime1(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + char out[sizeof(hc->switchtime1)] = {'\0'}; if (set_switchtime(value, timer_typeids[hc->hc()], out, sizeof(out))) { if (strlen(out)) { has_update(hc->switchtime1, out, sizeof(hc->switchtime1)); } + return true; } + return false; } @@ -3972,30 +4198,37 @@ bool Thermostat::set_switchtime2(const char * value, const int8_t id) { if (hc == nullptr) { return false; } + char out[sizeof(hc->switchtime2)] = {'\0'}; if (set_switchtime(value, timer_typeids[hc->hc()] + 3, out, sizeof(out))) { if (strlen(out)) { has_update(hc->switchtime2, out, sizeof(hc->switchtime2)); } + return true; } + return false; } + // sets a single switchtime in the thermostat dhw program for RC35 bool Thermostat::set_wwCircSwitchTime(const char * value, const int8_t id) { auto dhw = dhw_circuit(id2dhw(id)); if (dhw == nullptr) { return false; } + char out[sizeof(dhw->wwCircSwitchTime_)] = {'\0'}; if (set_switchtime(value, 0x39, out, sizeof(out))) { if (strlen(out)) { has_update(dhw->wwCircSwitchTime_, out, sizeof(dhw->wwCircSwitchTime_)); } + return true; } + return false; } @@ -4005,14 +4238,17 @@ bool Thermostat::set_wwSwitchTime(const char * value, const int8_t id) { if (dhw == nullptr) { return false; } + char out[sizeof(dhw->wwSwitchTime_)] = {'\0'}; if (set_switchtime(value, 0x38, out, sizeof(out))) { if (strlen(out)) { has_update(dhw->wwSwitchTime_, out, sizeof(dhw->wwSwitchTime_)); } + return true; } + return false; } @@ -4046,9 +4282,11 @@ bool Thermostat::set_program(const char * value, const int8_t id) { } else { write_command(set_typeids[hc->hc()], 13, set + 1, set_typeids[hc->hc()]); } + return true; } } + return false; } @@ -4080,6 +4318,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = 4; } + break; default: break; @@ -4123,6 +4362,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = EMS_OFFSET_RC20Set_temp_auto; } + break; default: break; @@ -4175,6 +4415,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = EMS_OFFSET_RC30Set_temp_auto; } + break; } @@ -4193,6 +4434,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co write_command(0x470, 1, temperature, 0x470); return true; } + if (is_received(summer2_typeids[hc->hc()])) { offset = 0x01; set_typeid = summer2_typeids[hc->hc()]; @@ -4200,6 +4442,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co offset = 0x06; set_typeid = summer_typeids[hc->hc()]; } + validate_typeid = set_typeid; factor = 1; break; @@ -4208,6 +4451,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co write_command(0x470, 5, temperature, 0x470); return true; } + offset = 5; set_typeid = summer2_typeids[hc->hc()]; validate_typeid = set_typeid; @@ -4219,12 +4463,14 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = 10; // manual offset } + break; case HeatingCircuit::Mode::TEMPAUTO: offset = 0x08; // auto offset if (temperature == -1) { factor = 1; // to write 0xFF } + break; case HeatingCircuit::Mode::COOLTEMP: offset = 0x11; @@ -4249,6 +4495,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = 4; } + factor = 1; break; case HeatingCircuit::Mode::BASEFLOW: @@ -4304,6 +4551,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co factor = 1; } } + validate_typeid = monitor_typeids[hc->hc()]; // get setpoint roomtemp back break; } @@ -4341,6 +4589,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = 13; // tempautotemp } + break; } @@ -4369,6 +4618,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = EMS_OFFSET_RC35Set_temp_design; } + factor = 1; break; case HeatingCircuit::Mode::SUMMER: @@ -4408,6 +4658,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co } else { offset = 15; } + factor = 1; break; default: @@ -4425,6 +4676,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co uint8_t modetype = hc->get_mode_type(); offset = (modetype == HeatingCircuit::Mode::NIGHT) ? EMS_OFFSET_RC35Set_temp_night : EMS_OFFSET_RC35Set_temp_day; } + break; } @@ -4480,6 +4732,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co offset = EMS_OFFSET_JunkersSetMessage_no_frost_temp; } } + break; } @@ -4521,6 +4774,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co offset = EMS_OFFSET_JunkersSetMessage2_no_frost_temp; } } + break; } } @@ -4537,6 +4791,7 @@ bool Thermostat::set_temperature(const float temperature, const uint8_t mode, co has_update(hc->selTemp, (int16_t)(temperature * factor)); // read_command(0x08, 0x18); // UBAMonitorFast } + return true; } @@ -4549,6 +4804,7 @@ bool Thermostat::set_temperature_value(const char * value, const int8_t id, cons if (Helpers::value2temperature(value, f, relative)) { return set_temperature(f, mode, id); } + return false; } @@ -4561,6 +4817,7 @@ void Thermostat::register_device_values() { register_device_value(tag, &tempsensor1_, DeviceValueType::INT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(RFTemp), DeviceValueUOM::DEGREES); return; } + // RC100H remote with humidity, this is also EMSdevice::EMS_DEVICE_FLAG_RC100 for set_calinttemp if (device_id() >= 0x38 && device_id() <= 0x3F) { // each device controls only one hc, so we tag the values @@ -4578,6 +4835,7 @@ void Thermostat::register_device_values() { register_device_value(tag, &battery_, DeviceValueType::UINT8, DeviceValueNumOp::DV_NUMOP_DIV2, FL_(battery), DeviceValueUOM::PERCENT); return; } + // Remotes RC20 and Junkers FB10, show only internal sensor if ((this->model() == EMSdevice::EMS_DEVICE_FLAG_RC20_N || this->model() == EMSdevice::EMS_DEVICE_FLAG_RC20 || this->model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) @@ -4586,6 +4844,7 @@ void Thermostat::register_device_values() { register_device_value(tag, &tempsensor1_, DeviceValueType::INT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(RFTemp), DeviceValueUOM::DEGREES); return; } + // Common for all thermostats register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &errorCode_, DeviceValueType::STRING, FL_(errorCode), DeviceValueUOM::NONE); register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &lastCode_, DeviceValueType::STRING, FL_(lastCode), DeviceValueUOM::NONE); @@ -4649,6 +4908,7 @@ void Thermostat::register_device_values() { DeviceValueUOM::NONE, MAKE_CF_CB(set_R3000Holiday)); } + register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &hybridStrategy_, DeviceValueType::ENUM, @@ -4978,6 +5238,7 @@ void Thermostat::register_device_values() { DeviceValueUOM::NONE, MAKE_CF_CB(set_language)); } + register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, &ibaBuildingType_, DeviceValueType::UINT8, @@ -5102,6 +5363,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrselTemp, DeviceValueType::INT16, seltemp_divider, FL_(selRoomTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_temp), 0, 30); } + register_device_value(tag, &hc->roomTemp, DeviceValueType::INT16, roomtemp_divider, FL_(roomTemp), DeviceValueUOM::DEGREES); register_device_value(tag, &hc->climate, DeviceValueType::ENUM, FL_(enum_climate), FL_(haclimate), DeviceValueUOM::NONE, nullptr, 5, 30); @@ -5127,6 +5389,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrmode, DeviceValueType::ENUM, FL_(enum_mode), FL_(mode), DeviceValueUOM::NONE, MAKE_CF_CB(set_mode)); } + register_device_value(tag, &hc->modetype, DeviceValueType::ENUM, FL_(enum_modetype), FL_(modetype), DeviceValueUOM::NONE); register_device_value( tag, &hc->nighttemp, DeviceValueType::UINT8, DeviceValueNumOp::DV_NUMOP_DIV2, FL_(ecotemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_ecotemp)); @@ -5179,6 +5442,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrcontrolmode, DeviceValueType::ENUM, FL_(enum_controlmode1), FL_(controlmode), DeviceValueUOM::NONE, MAKE_CF_CB(set_controlmode)); } + register_device_value(tag, &hc->program, DeviceValueType::ENUM, FL_(enum_progMode), FL_(program), DeviceValueUOM::NONE, MAKE_CF_CB(set_program)); register_device_value(tag, &hc->tempautotemp, @@ -5220,6 +5484,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrcontrol, DeviceValueType::ENUM, FL_(enum_control1), FL_(control), DeviceValueUOM::NONE, MAKE_CF_CB(set_control)); } + if (model != EMSdevice::EMS_DEVICE_FLAG_RC100 && model != EMSdevice::EMS_DEVICE_FLAG_CR120) { register_device_value(tag, &hc->remotetemp, @@ -5232,6 +5497,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrremotehum, DeviceValueType::CMD, FL_(remotehum), DeviceValueUOM::PERCENT, MAKE_CF_CB(set_remotehum), -1, 101); } + register_device_value(tag, &hc->heatondelay, DeviceValueType::UINT8, FL_(heatondelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_heatondelay), 1, 48); register_device_value(tag, &hc->heatoffdelay, DeviceValueType::UINT8, FL_(heatoffdelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_heatoffdelay), 1, 48); register_device_value(tag, &hc->coolondelay, DeviceValueType::UINT8, FL_(coolondelay), DeviceValueUOM::HOURS, MAKE_CF_CB(set_coolondelay), 1, 48); @@ -5522,6 +5788,7 @@ void Thermostat::register_device_values_hc(std::shared_ptrcontrol, DeviceValueType::ENUM, FL_(enum_j_control), FL_(control), DeviceValueUOM::NONE, MAKE_CF_CB(set_control)); } + register_device_value(tag, &hc->program, DeviceValueType::ENUM, FL_(enum_progMode4), FL_(program), DeviceValueUOM::NONE, MAKE_CF_CB(set_program)); register_device_value(tag, &hc->remotetemp, @@ -5595,6 +5862,7 @@ void Thermostat::register_device_values_dhw(std::shared_ptrwwMode_, DeviceValueType::ENUM, FL_(enum_wwMode), FL_(wwMode), DeviceValueUOM::NONE, MAKE_CF_CB(set_wwmode)); } + register_device_value(tag, &dhw->wwModeType_, DeviceValueType::ENUM, FL_(enum_wwModeType), FL_(modetype), DeviceValueUOM::NONE); register_device_value(tag, &dhw->wwSetTemp_, DeviceValueType::UINT8, FL_(wwSetTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_wwtemp)); register_device_value(tag, &dhw->wwSetTempLow_, DeviceValueType::UINT8, FL_(wwSetTempLow), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_wwtemplow)); diff --git a/src/devices/thermostat.h b/src/devices/thermostat.h index 1b3cf19cf..e2fe72b3c 100644 --- a/src/devices/thermostat.h +++ b/src/devices/thermostat.h @@ -32,6 +32,7 @@ class Thermostat : public EMSdevice { : hc_num_(hc_num) , model_(model) { } + ~HeatingCircuit() = default; int16_t selTemp; @@ -190,6 +191,7 @@ class Thermostat : public EMSdevice { DhwCircuit(const uint8_t offset) : dhw_num_(offset) { } + ~DhwCircuit() = default; uint8_t wwExtra_; uint8_t wwMode_; @@ -507,6 +509,7 @@ class Thermostat : public EMSdevice { inline bool set_vacation(const char * value, const int8_t id) { return set_holiday(value, id, true); } + bool set_pause(const char * value, const int8_t id); bool set_party(const char * value, const int8_t id); bool set_holidaymode(const char * value, const int8_t id); @@ -537,78 +540,103 @@ class Thermostat : public EMSdevice { inline bool set_temp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::AUTO); } + inline bool set_nighttemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::NIGHT); } + inline bool set_daytemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::DAY); } + inline bool set_daylowtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::DAYLOW); } + inline bool set_daymidtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::DAYMID); } + inline bool set_comforttemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::COMFORT); } + inline bool set_nofrosttemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::NOFROST); } + inline bool set_ecotemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::ECO); } + inline bool set_heattemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::HEAT); } + inline bool set_summertemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::SUMMER); } + inline bool set_designtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::DESIGN); } + inline bool set_offsettemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::OFFSET); } + inline bool set_holidaytemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::HOLIDAY); } + inline bool set_offtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::OFF); } + inline bool set_manualtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::MANUAL); } + inline bool set_tempautotemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::TEMPAUTO); } + inline bool set_noreducetemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::NOREDUCE); } + inline bool set_reducetemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::REDUCE); } + inline bool set_vacreducetemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::VACREDUCE); } + inline bool set_flowtempoffset(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::FLOWOFFSET, true); } + inline bool set_maxflowtemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::MAXFLOW); } + inline bool set_minflowtemp(const char * value, const int8_t id) { 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) { return set_temperature_value(value, id, HeatingCircuit::Mode::ROOMINFLUENCE, true); } + inline bool set_cooltemp(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::COOLTEMP); } + inline bool set_coolstart(const char * value, const int8_t id) { return set_temperature_value(value, id, HeatingCircuit::Mode::COOLSTART); } @@ -636,31 +664,40 @@ class Thermostat : public EMSdevice { inline bool set_wwVacation(const char * value, const int8_t id) { return set_holiday(value, DeviceValueTAG::TAG_DHW1, true); } + inline bool set_wwHoliday(const char * value, const int8_t id) { return set_holiday(value, DeviceValueTAG::TAG_DHW1); } + bool set_RC30Vacation(const char * value, const int8_t id); inline bool set_RC30Vacation1(const char * value, const int8_t id) { return set_RC30Vacation(value, 1); } + inline bool set_RC30Vacation2(const char * value, const int8_t id) { return set_RC30Vacation(value, 2); } + inline bool set_RC30Vacation3(const char * value, const int8_t id) { return set_RC30Vacation(value, 3); } + inline bool set_RC30Vacation4(const char * value, const int8_t id) { return set_RC30Vacation(value, 4); } + inline bool set_RC30Vacation5(const char * value, const int8_t id) { return set_RC30Vacation(value, 5); } + inline bool set_RC30Vacation6(const char * value, const int8_t id) { return set_RC30Vacation(value, 6); } + inline bool set_RC30Vacation7(const char * value, const int8_t id) { return set_RC30Vacation(value, 7); } + bool set_datetime(const char * value, const int8_t id); bool set_minexttemp(const char * value, const int8_t id); bool set_clockoffset(const char * value, const int8_t id); @@ -710,4 +747,4 @@ class Thermostat : public EMSdevice { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/devices/ventilation.cpp b/src/devices/ventilation.cpp index 2dbff72e4..c9905b1bd 100644 --- a/src/devices/ventilation.cpp +++ b/src/devices/ventilation.cpp @@ -97,6 +97,7 @@ bool Ventilation::set_ventMode(const char * value, const int8_t id) { if (!Helpers::value2enum(value, v, FL_(enum_ventMode))) { return false; } + write_command(0x56B, 0, v - 1, 0x56B); return true; } @@ -106,6 +107,7 @@ bool Ventilation::set_filter(const char * value, const int8_t id) { if (!Helpers::value2number(value, v)) { return false; } + // write_command(0x5xx, 0, v, 0x5xx); return true; } @@ -115,6 +117,7 @@ bool Ventilation::set_bypass(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x55C, 1, b ? 1 : 0, 0x587); return true; } diff --git a/src/devices/water.cpp b/src/devices/water.cpp index 1172f9203..d775c2342 100644 --- a/src/devices/water.cpp +++ b/src/devices/water.cpp @@ -76,6 +76,7 @@ Water::Water(uint8_t device_type, uint8_t device_id, uint8_t product_id, const c } else if (tag == DeviceValueTAG::TAG_DHW4) { register_telegram_type(0x07C3, "SM100wwCommand", false, MAKE_PF_CB(process_SM100wwCommand)); // command from dhw3->dhw4 } + // device values... register_device_value(tag, &wwTemp_, DeviceValueType::UINT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(wwTemp), DeviceValueUOM::DEGREES); register_device_value(tag, &wwTemp2_, DeviceValueType::UINT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(wwStorageTemp1), DeviceValueUOM::DEGREES); @@ -133,6 +134,7 @@ void Water::process_SM100wwParam(const std::shared_ptr & telegra // (daily heating time thermostat 2F5, offset 9, offset 8 on/off) has_update(telegram, errorDisp_, 19); } + // SM100wwParam2 - 0x07AC, Solar Module(0x2A) -> (0x00) void Water::process_SM100wwParam2(const std::shared_ptr & telegram) { has_update(telegram, wwDeltaTRet_, 1); @@ -248,6 +250,7 @@ bool Water::set_wwSelTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { write_command(0x35, 3, (uint8_t)temperature, 0x34); } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { @@ -255,6 +258,7 @@ bool Water::set_wwSelTemp(const char * value, const int8_t id) { } else { // SM100 write_command(0x7A6, 9, (uint8_t)temperature, 0x7A6); } + return true; } @@ -263,6 +267,7 @@ bool Water::set_wwMaxTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { return false; } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { @@ -270,6 +275,7 @@ bool Water::set_wwMaxTemp(const char * value, const int8_t id) { } else { // SM100 write_command(0x7A6, 8, (uint8_t)temperature, 0x7A6); } + return true; } @@ -278,6 +284,7 @@ bool Water::set_wwRedTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { return false; } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { @@ -285,6 +292,7 @@ bool Water::set_wwRedTemp(const char * value, const int8_t id) { } else { // SM100 write_command(0x7A6, 10, (uint8_t)temperature, 0x7A6); } + return true; } @@ -293,6 +301,7 @@ bool Water::set_wwHotTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + write_command(0x7A6, 7, (uint8_t)temperature, 0x7A6); return true; } @@ -302,6 +311,7 @@ bool Water::set_wwDailyTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + write_command(0x7A6, 6, (uint8_t)temperature, 0x7A6); return true; } @@ -311,6 +321,7 @@ bool Water::set_wwDisinfectionTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, temperature)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { write_command(0x33, 8, (uint8_t)temperature, 0x33); } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { @@ -318,6 +329,7 @@ bool Water::set_wwDisinfectionTemp(const char * value, const int8_t id) { } else { // SM100 write_command(0x7A6, 12, (uint8_t)temperature, 0x7A6); } + return true; } @@ -326,6 +338,7 @@ bool Water::set_wwCirc(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { write_command(0x33, 6, b ? 0xFF : 0x00, 0x33); } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { @@ -333,6 +346,7 @@ bool Water::set_wwCirc(const char * value, const int8_t id) { } else { // SM100 write_command(0x7A5, 0, b ? 0xFF : 0x00, 0x7A5); } + return true; } @@ -341,6 +355,7 @@ bool Water::set_wwCircMode(const char * value, const int8_t id) { if (!Helpers::value2enum(value, num, FL_(enum_wwCircMode))) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_IPM) { write_command(0x33, 7, num, 0x33); } else if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { @@ -348,6 +363,7 @@ bool Water::set_wwCircMode(const char * value, const int8_t id) { } else { // SM100 write_command(0x7A5, 3, num, 0x7A5); } + return true; } @@ -357,11 +373,13 @@ bool Water::set_wwCircTc(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { write_command(0x33B + dhw_, 4, b ? 0x01 : 0x00, 0x33B + dhw_); } else { // SM100 write_command(0x7A5, 4, b ? 0xFF : 0x00, 0x7A5); } + return true; } @@ -370,6 +388,7 @@ bool Water::set_wwKeepWarm(const char * value, const int8_t id) { if (!Helpers::value2bool(value, b)) { return false; } + write_command(0x7AE, 0, b ? 0xFF : 0x00, 0x7AE); return true; } @@ -379,10 +398,12 @@ bool Water::set_wwDiffTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { write_command(0x313 + dhw_, 7, (int8_t)(v * 10), 0x313 + dhw_); return true; } + return false; } @@ -391,10 +412,12 @@ bool Water::set_wwRequiredTemp(const char * value, const int8_t id) { if (!Helpers::value2temperature(value, v)) { return false; } + if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) { write_command(0x313 + dhw_, 4, (uint8_t)v, 0x313 + dhw_); return true; } + return false; } @@ -403,6 +426,7 @@ bool Water::set_wwFlowTempOffset(const char * value, const int8_t id) { if (!Helpers::value2number(value, n)) { return false; } + write_command(0x33, 5, n, 0x33); return true; } @@ -412,6 +436,7 @@ bool Water::set_wwHystOn(const char * value, const int8_t id) { if (!Helpers::value2number(value, n)) { return false; } + write_command(0x33, 3, n, 0x33); return true; } @@ -421,6 +446,7 @@ bool Water::set_wwHystOff(const char * value, const int8_t id) { if (!Helpers::value2number(value, n)) { return false; } + write_command(0x33, 4, n, 0x33); return true; } @@ -430,6 +456,7 @@ bool Water::set_wwRetValve(const char * value, const int8_t id) { if (!Helpers::value2number(value, n)) { return false; } + write_command(0x7AD, 1, n, 0x7AD); return true; } @@ -439,6 +466,7 @@ bool Water::set_errorDisp(const char * value, const int8_t id) { if (!Helpers::value2enum(value, n, FL_(enum_errorDisp))) { return false; } + write_command(0x7A6, 19, n, 0x7A6); return true; } @@ -448,6 +476,7 @@ bool Water::set_wwDeltaTRet(const char * value, const int8_t id) { if (!Helpers::value2number(value, n)) { return false; } + write_command(0x7AC, 1, n, 0x7AC); return true; } diff --git a/src/test/test.cpp b/src/test/test.cpp index 8e38974dd..003d53896 100644 --- a/src/test/test.cpp +++ b/src/test/test.cpp @@ -316,6 +316,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const id1 = Helpers::atoint(id1_s.c_str()); } } + if (!id2_s.empty()) { id2 = Helpers::atoint(id2_s.c_str()); } @@ -326,6 +327,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const shell.printfln("Usage: test add "); return; } + shell.printfln("Testing adding a device (deviceID 0x%02X, product_id %d), with all values...", id1, id2); test("add", id1, id2); shell.invoke_command("show devices"); @@ -694,6 +696,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const if (json.size() != 0) { serializeJson(doc, Serial); } + shell.println(); Serial.print(COLOR_RESET); @@ -715,6 +718,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const Serial.println(" **"); } } + ok = true; } @@ -983,6 +987,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const if (id1 == -1) { id1 = 0; } + shell.printfln("Testing healthcheck with %d", id1); EMSESP::system_.healthcheck(id1); ok = true; @@ -2212,6 +2217,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const if (!pass) { failed++; } + shell.printfln("%s %-14s -> %-14s expected %-9s got %-9s", pass ? "PASS" : "FAIL", test.settings, test.current, test.expected, actual); } @@ -2238,6 +2244,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const snprintf(value, 7, "val%03d", i); doc[key] = value; } + doc.shrinkToFit(); JsonObject jo = doc.as(); shell.printfln("Length of JSON payload = %d", measureJson(jo)); @@ -2528,11 +2535,13 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const for (const auto & d : response._data) { shell.printf(" %d", d); } + if (response._data.size() == 5 && response._data[3] == 0 && response._data[4] == (uint8_t)EMS_VALUE_DEFAULT_INT8_DUMMY) { shell.printf(" [OK]"); } else { shell.printf(" [ERROR - invalid response]"); } + shell.println(); } else { shell.printf("mintempsilent [MODBUS ERROR %d]\n", response.getError()); @@ -2553,6 +2562,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const for (const auto & d : response._data) { shell.printf(" %d", d); } + shell.println(" [OK]"); } else { shell.printf("selflowtemp [MODBUS ERROR %d]\n", response.getError()); @@ -2574,6 +2584,7 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd, const for (const auto & d : response._data) { shell.printf(" %d", d); } + shell.println(" [OK]"); } else { shell.printf("remotetemp [MODBUS ERROR %d]\n", response.getError()); @@ -2691,6 +2702,7 @@ void Test::rx_telegram(const std::vector & rx_data) { data[i] = rx_data[i]; i++; } + data[i] = EMSESP::rxservice_.calculate_crc(data, i); EMSESP::rxservice_.add(data, len + 1); @@ -2706,6 +2718,7 @@ void Test::uart_telegram(const std::vector & rx_data) { data[i] = rx_data[i]; i++; } + data[i] = EMSESP::rxservice_.calculate_crc(data, i); EMSESP::incoming_telegram(data, i + 1); @@ -2719,6 +2732,7 @@ void Test::uart_telegram_withCRC(const char * rx_data) { for (uint8_t i = 0; i < strlen(rx_data); i++) { telegram[i] = rx_data[i]; } + telegram[strlen(rx_data)] = '\0'; // make sure its terminated uint8_t count = 0; @@ -2758,6 +2772,7 @@ void Test::uart_telegram(const char * rx_data) { for (uint8_t i = 0; i < strlen(rx_data); i++) { telegram[i] = rx_data[i]; } + telegram[strlen(rx_data)] = '\0'; // make sure its terminated uint8_t count = 0; diff --git a/src/uart/emsuart_esp32.cpp b/src/uart/emsuart_esp32.cpp index d1337d44b..e527de6a5 100644 --- a/src/uart/emsuart_esp32.cpp +++ b/src/uart/emsuart_esp32.cpp @@ -54,6 +54,7 @@ void EMSuart::uart_event_task(void * pvParameters) { if (length == 2 || (length >= 6 && length <= EMS_MAXBUFFERSIZE)) { EMSESP::incoming_telegram(telegram, (uint8_t)(length - 1)); } + length = 0; } else if (event.type == UART_BUFFER_FULL) { uart_flush_input(EMSUART_NUM); @@ -61,6 +62,7 @@ void EMSuart::uart_event_task(void * pvParameters) { } } } + vTaskDelete(NULL); } @@ -118,6 +120,7 @@ void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t gpio_pullup_en((gpio_num_t)rx_gpio); vTaskResume(xHandle); } + tx_mode_ = tx_mode; uart_enable_intr_mask(EMSUART_NUM, UART_BRK_DET_INT_ENA | UART_RXFIFO_FULL_INT_ENA); } @@ -181,6 +184,7 @@ uint8_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { uart_write_bytes(EMSUART_NUM, &buf[i], 1); delayMicroseconds(EMSUART_TX_WAIT_PLUS); } + uart_gen_break(EMSUART_TX_BRK_PLUS); return EMS_TX_STATUS_OK; } @@ -191,6 +195,7 @@ uint8_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { uart_write_bytes(EMSUART_NUM, &buf[i], 1); delayMicroseconds(EMSUART_TX_WAIT_HT3); } + uart_gen_break(EMSUART_TX_BRK_HT3); return EMS_TX_STATUS_OK; } @@ -206,6 +211,7 @@ uint8_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) { uart_get_buffered_data_len(EMSUART_NUM, &rx1); } while ((rx1 == rx0) && (--timeoutcnt)); } + uart_gen_break(EMSUART_TX_BRK_EMS); return EMS_TX_STATUS_OK; } diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 7b74b64a1..d7c92ce12 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -44,6 +44,7 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request, JsonVariant j request->send(429); // too many requests return; } + busy = true; JsonDocument input_doc; // has no body JSON so create dummy as empty input object JsonObject input; @@ -58,6 +59,7 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request, JsonVariant j input = input_doc.to(); input["data"] = json.as(); } + parse(request, input); busy = false; } @@ -95,21 +97,27 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { if (request->hasParam(F_(device))) { input["device"] = request->getParam(F_(device))->value().c_str(); } + if (request->hasParam(F_(cmd))) { input["cmd"] = request->getParam(F_(cmd))->value().c_str(); } + if (request->hasParam(F_(data))) { input["data"] = request->getParam(F_(data))->value().c_str(); } + if (request->hasParam(F_(value))) { input["value"] = request->getParam(F_(value))->value().c_str(); } + if (request->hasParam(F_(id))) { input["id"] = Helpers::atoint(request->getParam(F_(id))->value().c_str()); } + if (request->hasParam(F_(hc))) { input["hc"] = Helpers::atoint(request->getParam(F_(hc))->value().c_str()); } + if (request->hasParam(F_(dhw))) { input["dhw"] = Helpers::atoint(request->getParam(F_(dhw))->value().c_str()); } @@ -205,6 +213,7 @@ void WebAPIService::storeResponse(JsonObject response) { storeResponseDoc_.clear(); // clear it, so can only recall once storeResponseDoc_.add(response); // add the object to our doc } + const char * WebAPIService::getResponse() { static std::string buffer; serializeJson(storeResponseDoc_, buffer); diff --git a/src/web/WebActivityService.cpp b/src/web/WebActivityService.cpp index f2b3c8acd..883e8e8d2 100644 --- a/src/web/WebActivityService.cpp +++ b/src/web/WebActivityService.cpp @@ -59,6 +59,7 @@ void WebActivityService::webActivityService(AsyncWebServerRequest * request) { statJson["q"] = EMSESP::temperaturesensor_.reads() == 0 ? 100 : 100 - (uint8_t)((100 * EMSESP::temperaturesensor_.fails()) / EMSESP::temperaturesensor_.reads()); } + if (EMSESP::analog_enabled()) { statJson = statsJson.add(); statJson["id"] = 4; @@ -66,6 +67,7 @@ void WebActivityService::webActivityService(AsyncWebServerRequest * request) { statJson["f"] = EMSESP::analogsensor_.fails(); statJson["q"] = EMSESP::analogsensor_.reads() == 0 ? 100 : 100 - (uint8_t)((100 * EMSESP::analogsensor_.fails()) / EMSESP::analogsensor_.reads()); } + if (Mqtt::enabled()) { statJson = statsJson.add(); statJson["id"] = 5; @@ -100,4 +102,4 @@ void WebActivityService::webActivityService(AsyncWebServerRequest * request) { request->send(response); } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/web/WebCommandService.cpp b/src/web/WebCommandService.cpp index 62db7a08e..f71de590e 100644 --- a/src/web/WebCommandService.cpp +++ b/src/web/WebCommandService.cpp @@ -73,6 +73,7 @@ bool WebCommandService::queueCommand(const char * name, const char * value) { if (job == nullptr) { return false; } + job->name = name ? name : ""; job->has_value = (value != nullptr); job->value = value ? value : ""; @@ -81,6 +82,7 @@ bool WebCommandService::queueCommand(const char * name, const char * value) { delete job; return false; } + return true; } #endif @@ -95,6 +97,7 @@ bool WebCommandService::isUrlCommand(const std::string & command) { if (deserializeJson(doc, command) != DeserializationError::Ok) { return false; } + std::string url = doc["url"] | ""; auto lower_url = Helpers::toLower(url.c_str()); return lower_url.starts_with("http://") || lower_url.starts_with("https://"); @@ -123,6 +126,7 @@ bool WebCommandService::valueContainsUrl(const std::string & value) { i++; } } + JsonDocument doc; if (deserializeJson(doc, value.substr(f, e - f)) == DeserializationError::Ok) { for (JsonPairConst p : doc.as()) { @@ -131,8 +135,10 @@ bool WebCommandService::valueContainsUrl(const std::string & value) { } } } + f = value.find_first_of('{', e); } + return false; } @@ -149,6 +155,7 @@ bool WebCommandService::dispatchCommand(const char * name, const char * value) { if (isUrlCommand(ci->cmd.c_str())) { return queueCommand(name, value); } + // internal command whose value embeds a {url} fetch (e.g. system/message) - the value is // resolved by compute() at execution time and would block, so offload it to the worker task // the effective value is the override if given, else the command's stored default @@ -209,6 +216,7 @@ StateUpdateResult WebCommands::update(JsonObject root, WebCommands & webCommands FL_(command_cmd), CommandFlag::ADMIN_ONLY); } + return StateUpdateResult::CHANGED; } @@ -217,12 +225,14 @@ const CommandItem * WebCommandService::find(const char * name) { if (name == nullptr || name[0] == '\0') { return nullptr; } + auto lower_name = Helpers::toLower(name); for (const CommandItem & ci : *commandItems_) { if (ci.name[0] != '\0' && Helpers::toLower(ci.name) == lower_name) { return &ci; } } + return nullptr; } @@ -233,6 +243,7 @@ bool WebCommandService::executeCommand(const char * name, const char * value) { EMSESP::logger().warning("Command '%s' not found", name ? name : ""); return false; } + // if there is a value use it, otherwise use the command's default value std::string cmd_value = value ? value : ci->value.c_str(); return executeCommand(ci->name, std::string(ci->cmd.c_str()), cmd_value); @@ -268,6 +279,7 @@ bool WebCommandService::executeCommand(const char * name, const std::string & co commands(s, false); url.replace(q + 1, l, s); } + // the cmd's embedded value only gets entity substitution (commands), the passed value is fully computed std::string value = doc["value"] | computed_data; std::string method = doc["method"] | "GET"; @@ -308,6 +320,7 @@ bool WebCommandService::executeCommand(const char * name, const std::string & co if (data.empty() && output.size()) { Mqtt::queue_publish("response", output); } + return true; } @@ -317,6 +330,7 @@ bool WebCommandService::executeCommand(const char * name, const std::string & co } else { snprintf(error, sizeof(error), "Command '%s': %s failed with error %s", name, cmd.c_str(), Command::return_code_string(return_code)); } + EMSESP::logger().warning(error); return false; } @@ -332,6 +346,7 @@ bool WebCommandService::get_value_info(JsonObject output, const char * cmd) { output[(const char *)ci.name] = ci.value.c_str(); } } + return true; } @@ -341,6 +356,7 @@ bool WebCommandService::get_value_info(JsonObject output, const char * cmd) { output[(const char *)ci.name] = std::string(ci.cmd + ": " + ci.value); } } + return true; } @@ -350,6 +366,7 @@ bool WebCommandService::get_value_info(JsonObject output, const char * cmd) { get_value_json(output[ci.name].to(), ci); } } + return true; } @@ -359,6 +376,7 @@ bool WebCommandService::get_value_info(JsonObject output, const char * cmd) { output["api_data"] = metrics; return true; } + return false; } @@ -381,10 +399,12 @@ std::string WebCommandService::get_metrics_prometheus() { if (ci.name[0] == '\0') { continue; } + result += (std::string) "# HELP emsesp_cmd_" + ci.name + " " + ci.name + ", readable, writable, visible\n"; result += (std::string) "# TYPE emsesp_cmd_" + ci.name + " gauge\n"; result += (std::string) "emsesp_cmd_" + ci.name + " 1\n"; } + return result; } @@ -403,6 +423,7 @@ void WebCommandService::publish(const bool force) { if (!Mqtt::enabled() || commandItems_->empty()) { return; } + if (force && !EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { return; } diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index 227a28253..2cb2cf1b6 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -74,6 +74,7 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web if (entityItem.raw) { delete[] entityItem.raw; } + if (entityItem.ram == 2) { // NVS char key[sizeof(entityItem.name) + 2]; snprintf(key, sizeof(key), "c:%s", entityItem.name); @@ -81,10 +82,12 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web EMSESP::nvs_.remove(key); } } + if (entityItem.ram) { // save name/value pairs for change checking doc[entityItem.name] = entityItem.value; } } + webCustomEntity.customEntityItems.clear(); EMSESP::webCustomEntityService.ha_reset(); @@ -110,6 +113,7 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web entityItem.value_type = DeviceValueType::STRING; entityItem.writeable = true; } + entityItem.raw = nullptr; if (entityItem.ram == 0 && entityItem.value_type == DeviceValueType::STRING) { entityItem.raw = new uint8_t[(size_t)entityItem.factor + 1]; @@ -134,6 +138,7 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web if (entityItem.factor == 0) { entityItem.factor = 1; } + if (entityItem.ram == 2) { // NVS char key[sizeof(entityItem.name) + 2]; snprintf(key, sizeof(key), "c:%s", entityItem.name); @@ -141,6 +146,7 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web entityItem.data = EMSESP::nvs_.getString(key).c_str(); } } + webCustomEntity.customEntityItems.push_back(entityItem); // add to list if (entityItem.writeable && entityItem.name[0] != '\0') { @@ -161,6 +167,7 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web } } } + return StateUpdateResult::CHANGED; } @@ -179,6 +186,7 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i if (entityItem.data == value) { return true; // no change } + if (entityItem.ram == 1) { entityItem.data = value; } else if (entityItem.ram == 2) { // NVS @@ -198,6 +206,7 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i data[count++] = (uint8_t)strtol(p, 0, 16); p = strtok(nullptr, " ,"); } + free(telegram); uint8_t offset = entityItem.offset; uint8_t * dat = data; @@ -208,6 +217,7 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i count -= len; dat += len; } + delete[] data; // validate telegram EMSESP::send_read_request(entityItem.type_id, entityItem.device_id, entityItem.offset, length); @@ -217,12 +227,14 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i if (!Helpers::value2bool(value, v)) { return false; } + EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v ? (uint8_t)entityItem.factor : 0, entityItem.type_id); } else { float f; if (!Helpers::value2float(value, f)) { return false; } + int v = (f / entityItem.factor + 0.5); if (entityItem.value_type == DeviceValueType::UINT8 || entityItem.value_type == DeviceValueType::INT8) { EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v, entityItem.type_id); @@ -239,12 +251,14 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i if (EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { publish(); } + char cmd[COMMAND_MAX_LENGTH]; snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entityItem.name); EMSESP::webSchedulerService.onChange(cmd); return true; } } + return false; } @@ -263,30 +277,35 @@ void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem co Mqtt::add_value_bool(output, name, (uint8_t)entity.value != 0); } } + break; case DeviceValueType::INT8: if ((int8_t)entity.value != EMS_VALUE_INT8_NOTSET) { std::string v = Helpers::render_value(payload, entity.factor * (int8_t)entity.value, 2); output[name] = add_uom ? serialized(v + ' ' + EMSdevice::uom_to_string(entity.uom)) : serialized(v); } + break; case DeviceValueType::UINT8: if ((uint8_t)entity.value != EMS_VALUE_UINT8_NOTSET) { std::string v = Helpers::render_value(payload, entity.factor * (int8_t)entity.value, 2); output[name] = add_uom ? serialized(v + ' ' + EMSdevice::uom_to_string(entity.uom)) : serialized(v); } + break; case DeviceValueType::INT16: if ((int16_t)entity.value != EMS_VALUE_INT16_NOTSET) { std::string v = Helpers::render_value(payload, entity.factor * (int16_t)entity.value, 2); output[name] = add_uom ? serialized(v + ' ' + EMSdevice::uom_to_string(entity.uom)) : serialized(v); } + break; case DeviceValueType::UINT16: if ((uint16_t)entity.value != EMS_VALUE_UINT16_NOTSET) { std::string v = Helpers::render_value(payload, entity.factor * (uint16_t)entity.value, 2); output[name] = add_uom ? serialized(v + ' ' + EMSdevice::uom_to_string(entity.uom)) : serialized(v); } + break; case DeviceValueType::UINT24: case DeviceValueType::TIME: @@ -295,6 +314,7 @@ void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem co std::string v = Helpers::render_value(payload, entity.factor * entity.value, 2); output[name] = add_uom ? serialized(v + ' ' + EMSdevice::uom_to_string(entity.uom)) : serialized(v); } + break; // case DeviceValueType::STRING: default: @@ -302,6 +322,7 @@ void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem co if (entity.data.length() > 0) { output[name] = add_uom ? entity.data + ' ' + EMSdevice::uom_to_string(entity.uom) : entity.data; } + break; } } @@ -335,6 +356,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) render_value(output, entity); } } + return true; } @@ -344,6 +366,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) auto nest = output[entity.name].to(); get_value_json(nest, entity); } + return true; } @@ -353,6 +376,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) output["api_data"] = metrics; return true; } + return false; } @@ -364,6 +388,7 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) return Command::get_attribute(output, cmd, attribute_s); } } + return false; // not found } @@ -376,10 +401,12 @@ std::string WebCustomEntityService::get_metrics_prometheus() { 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 + " "; @@ -408,10 +435,13 @@ std::string WebCustomEntityService::get_metrics_prometheus() { if (entity.data.length() > 0) { result += entity.data; } + break; } + result += (std::string) "\n"; } + return result; } @@ -466,11 +496,13 @@ void WebCustomEntityService::publish(const bool force) { if (!Mqtt::enabled() || customEntityItems_->empty()) { return; } + if (force) { if (Mqtt::publish_single()) { for (CustomEntityItem & entityItem : *customEntityItems_) { publish_single(entityItem); } + return; } else if (!EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { return; // wait for first time period @@ -485,6 +517,7 @@ void WebCustomEntityService::publish(const bool force) { if (entityItem.hide) { continue; } + render_value(output, entityItem); // create HA config if (Mqtt::ha_enabled() && !ha_configdone_) { @@ -524,6 +557,7 @@ void WebCustomEntityService::publish(const bool force) { } else { snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } + char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(custom), entityItem.name); config["cmd_t"] = command_topic; @@ -559,6 +593,7 @@ void WebCustomEntityService::publish(const bool force) { snprintf(topic, sizeof(topic), "%s_data", F_(custom)); Mqtt::queue_publish(topic, output); } + // EMSESP::logger().debug("publish %d custom entities", output.size()); } @@ -591,6 +626,7 @@ void WebCustomEntityService::generate_value_web(JsonObject output, const bool is if (entity.hide) { continue; } + bool include = false; JsonObject root_obj = nodes.add(); // create the object, we know there is a value @@ -627,29 +663,34 @@ void WebCustomEntityService::generate_value_web(JsonObject output, const bool is l.add(Helpers::render_boolean(s, true, true)); break; } + case DeviceValueType::INT8: if ((int8_t)entity.value != EMS_VALUE_INT8_NOTSET) { obj["v"] = Helpers::transformNumFloat(entity.factor * (int8_t)entity.value); include = true; } + break; case DeviceValueType::UINT8: if ((uint8_t)entity.value != EMS_VALUE_UINT8_NOTSET) { obj["v"] = Helpers::transformNumFloat(entity.factor * (uint8_t)entity.value); include = true; } + break; case DeviceValueType::INT16: if ((int16_t)entity.value != EMS_VALUE_INT16_NOTSET) { obj["v"] = Helpers::transformNumFloat(entity.factor * (int16_t)entity.value); include = true; } + break; case DeviceValueType::UINT16: if ((uint16_t)entity.value != EMS_VALUE_UINT16_NOTSET) { obj["v"] = Helpers::transformNumFloat(entity.factor * (uint16_t)entity.value); include = true; } + break; case DeviceValueType::UINT24: case DeviceValueType::TIME: @@ -658,12 +699,14 @@ void WebCustomEntityService::generate_value_web(JsonObject output, const bool is obj["v"] = Helpers::transformNumFloat(entity.factor * entity.value); include = true; } + break; case DeviceValueType::STRING: if (entity.data.length() > 0) { obj["v"] = entity.data; include = true; } + break; default: break; @@ -696,6 +739,7 @@ void WebCustomEntityService::fetch() { break; } } + if (needFetch) { EMSESP::send_read_request(entity.type_id, entity.device_id, @@ -722,6 +766,7 @@ bool WebCustomEntityService::get_value(const std::shared_ptr & t message_length -= entity.offset - telegram->offset; offset = 0; } + auto length = std::min(offset + message_length, (int)entity.factor); auto rest = std::min((int)entity.factor - offset, (int)message_length); if (rest > 0) { @@ -734,6 +779,7 @@ bool WebCustomEntityService::get_value(const std::shared_ptr & t } else if (EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { has_change = true; } + char cmd[COMMAND_MAX_LENGTH]; snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entity.name); EMSESP::webSchedulerService.onChange(cmd); @@ -745,10 +791,12 @@ bool WebCustomEntityService::get_value(const std::shared_ptr & t for (uint8_t i = 0; i < len[entity.value_type]; i++) { value = (value << 8) + telegram->message_data[i + entity.offset - telegram->offset]; } + // mask bits for bool values if (entity.value_type == DeviceValueType::BOOL && entity.factor > 0) { value = (value & (uint8_t)entity.factor) ? 1 : 0; } + if (value != entity.value) { entity.value = value; if (Mqtt::publish_single()) { @@ -756,10 +804,12 @@ bool WebCustomEntityService::get_value(const std::shared_ptr & t } else if (EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { has_change = true; } + char cmd[COMMAND_MAX_LENGTH]; snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entity.name); EMSESP::webSchedulerService.onChange(cmd); } + // EMSESP::logger().debug("custom entity %s received with value %d", entity.name, (int)entity.val); } } diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index e06871920..74974701d 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -110,6 +110,7 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c *p = '_'; } } + customizations.sensorCustomizations.push_back(sensor); // add to list } } @@ -126,6 +127,7 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c EMSESP::logger().warning("Analog sensor: Invalid GPIO %d for %s. Skipping.", analogJson["gpio"].as(), analog_sensor_name); continue; } + auto analog = AnalogCustomization(); strlcpy(analog.name, analogJson["name"].as(), sizeof(analog.name)); analog.gpio = analogJson["gpio"]; @@ -137,6 +139,7 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c if (_start && analog.type == EMSESP::analogsensor_.AnalogType::DIGITAL_OUT && analog.uom > DeviceValue::DeviceValueUOM::NONE) { analog.offset = analog.uom > 1 ? 1 : 0; } + customizations.analogCustomizations.push_back(analog); // add to list } } @@ -332,11 +335,13 @@ void WebCustomizationService::customization_entities(AsyncWebServerRequest * req break; } } + if (!is_set) { entity_ids.push_back(entity_id); } } } + break; } } @@ -499,6 +504,7 @@ void WebCustomizationService::load_test_data() { dv.state = DeviceValueState::DV_FAVORITE; // set as favorite } } + break; } } diff --git a/src/web/WebCustomizationService.h b/src/web/WebCustomizationService.h index 50e6fe640..38eff7c38 100644 --- a/src/web/WebCustomizationService.h +++ b/src/web/WebCustomizationService.h @@ -55,6 +55,7 @@ class AnalogCustomization { bool operator==(const AnalogCustomization & a) const { return gpio == a.gpio; } + bool operator!=(const AnalogCustomization & a) const { return !operator==(a); } @@ -111,4 +112,4 @@ class WebCustomizationService : public StatefulService { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index 85d58858b..534541b12 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -115,15 +115,18 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) { if (Helpers::hasValue(sensor.temperature_c)) { obj["t"] = (float)sensor.temperature_c * 0.18 + 32; } + obj["u"] = DeviceValueUOM::FAHRENHEIT; obj["o"] = (float)sensor.offset() * 0.18; } else { if (Helpers::hasValue(sensor.temperature_c)) { obj["t"] = (float)sensor.temperature_c / 10; } + obj["u"] = DeviceValueUOM::DEGREES; obj["o"] = (float)(sensor.offset()) / 10; } + obj["s"] = sensor.is_system(); } } @@ -188,6 +191,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request) { for (uint16_t i = 0; i < (TxService::POST_SEND_DELAY + 500) && EMSESP::wait_validate(); i++) { delay(1); } + EMSESP::wait_validate(0); // reset in case of timeout #ifndef EMSESP_STANDALONE JsonObject output = response->getRoot(); @@ -263,14 +267,17 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar break; } } + break; } + if (device_type == EMSdevice::DeviceType::UNKNOWN) { EMSESP::logger().warning("Write command failed, bad device id: %d", unique_id); AsyncWebServerResponse * response = request->beginResponse(400); // bad request request->send(response); return; } + // create JSON for output auto * response = new PsramAsyncJsonResponse(false); JsonObject output = response->getRoot(); @@ -282,6 +289,7 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar if (device_type >= EMSdevice::DeviceType::BOILER) { cmd = Command::parse_command_string(cmd, id); // extract hc or dhw } + if (data.is()) { return_code = Command::call(device_type, cmd, data.as(), true, id, output); } else if (data.is()) { @@ -413,6 +421,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { if (sensor.is_system()) { continue; } + JsonObject node = nodes.add(); node["id"] = (EMSdevice::DeviceTypeUniqueID::TEMPERATURESENSOR_UID * 100) + count++; @@ -422,11 +431,13 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { if (Helpers::hasValue(sensor.temperature_c)) { dv["v"] = (float)sensor.temperature_c * 0.18 + 32; } + dv["u"] = DeviceValueUOM::FAHRENHEIT; } else { if (Helpers::hasValue(sensor.temperature_c)) { dv["v"] = (float)sensor.temperature_c / 10; } + dv["u"] = DeviceValueUOM::DEGREES; } } @@ -444,6 +455,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { if (sensor.is_system()) { continue; } + JsonObject node = nodes.add(); node["id"] = (EMSdevice::DeviceTypeUniqueID::ANALOGSENSOR_UID * 100) + count++; @@ -469,6 +481,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { dv["v"] = Helpers::transformNumFloat(sensor.value()); dv["u"] = sensor.uom(); } + if (sensor.type() == AnalogSensor::AnalogType::COUNTER || (sensor.type() >= AnalogSensor::AnalogType::DIGITAL_OUT && sensor.type() <= AnalogSensor::AnalogType::PWM_2) || sensor.type() == AnalogSensor::AnalogType::RGB || sensor.type() == AnalogSensor::AnalogType::PULSE @@ -535,4 +548,4 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { request->send(response); } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/web/WebLogService.cpp b/src/web/WebLogService.cpp index 6116ee779..8f1acab31 100644 --- a/src/web/WebLogService.cpp +++ b/src/web/WebLogService.cpp @@ -48,6 +48,7 @@ void WebLogService::start() { if (settings.weblog_buffer) { maximum_log_messages_ = settings.weblog_buffer; } + limit_log_messages_ = maximum_log_messages_; compact_ = settings.weblog_compact; level_ = (uuid::log::Level)settings.weblog_level; @@ -166,6 +167,7 @@ char * WebLogService::messagetime(char * out, const uint64_t t, const size_t buf strftime(timestr, 25, "%FT%T", localtime(&t1)); snprintf(out, bufsize, "%s.%03d", timestr, (int)(wall_ms % 1000)); } + return out; } @@ -188,6 +190,7 @@ void WebLogService::transmit(const QueuedLogMessage & message) { if (scratch_buf_.capacity() < len + 1) { scratch_buf_.reserve(len + 1); } + scratch_buf_.resize(len); serializeJson(jsonDocument, scratch_buf_.data(), len + 1); events_.send(scratch_buf_.c_str(), "message", message.id_); @@ -223,6 +226,7 @@ void WebLogService::getSetValues(AsyncWebServerRequest * request, JsonVariant js if (max_messages) { maximum_log_messages_ = max_messages; } + compact_ = json["compact"]; uuid::log::Logger::register_handler(this, level_); @@ -247,4 +251,4 @@ void WebLogService::getSetValues(AsyncWebServerRequest * request, JsonVariant js request->send(200); // OK } -} // namespace emsesp \ No newline at end of file +} // namespace emsesp diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index b6c2b7b13..04b3690b1 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -71,6 +71,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu EMSESP::nvs_.remove(key); } } + webScheduler.scheduleItems.clear(); EMSESP::webSchedulerService.ha_reset(); @@ -95,6 +96,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu if (EMSESP::nvs_.isKey(key)) { webScheduler.scheduleItems.back().active = EMSESP::nvs_.getBool(key); } + Command::add( EMSdevice::DeviceType::SCHEDULER, webScheduler.scheduleItems.back().name, @@ -104,6 +106,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu FL_(schedule_cmd), CommandFlag::ADMIN_ONLY); } + return StateUpdateResult::CHANGED; } @@ -134,6 +137,7 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id, return true; } } + return false; } @@ -150,6 +154,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { Mqtt::add_value_bool(output, scheduleItem.name, scheduleItem.active); } } + return true; } @@ -160,6 +165,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { strlcpy(name, scheduleItem.name[0] == '\0' ? Helpers::smallitoa(name, i++) : scheduleItem.name, sizeof(name)); get_value_json(output[name].to(), scheduleItem); } + return true; } @@ -169,6 +175,7 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { output["api_data"] = metrics; return true; } + return false; } @@ -191,10 +198,12 @@ std::string WebSchedulerService::get_metrics_prometheus() { 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; } @@ -213,6 +222,7 @@ void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & } else { output["time"] = scheduleItem.time; } + output["cmd_name"] = scheduleItem.cmd_name; output["readable"] = true; output["writeable"] = true; @@ -241,11 +251,13 @@ void WebSchedulerService::publish(const bool force) { if (!Mqtt::enabled() || scheduleItems_->empty()) { return; } + if (force) { if (Mqtt::publish_single()) { for (const ScheduleItem & scheduleItem : *scheduleItems_) { publish_single(scheduleItem.name, scheduleItem.active); } + return; } else if (!EMSESP::mqtt_.get_publish_onchange(EMSdevice::DeviceType::SYSTEM)) { return; // wait for first time period @@ -279,6 +291,7 @@ void WebSchedulerService::publish(const bool force) { } else { snprintf(val_tpl, sizeof(val_tpl), "{{%s}}", val_obj); // omit value conditional Jinja2 template code } + config["val_tpl"] = val_tpl; char uniq_s[70]; @@ -328,6 +341,7 @@ bool WebSchedulerService::runScheduleCommand(const ScheduleItem & si) { EMSESP::logger().warning("Schedule '%s': no command assigned", si.name); return false; } + return EMSESP::webCommandService.dispatchCommand(si.cmd_name.c_str()); } @@ -339,6 +353,7 @@ bool WebSchedulerService::onChange(const char * cmd) { return true; } } + return false; } @@ -369,6 +384,7 @@ void WebSchedulerService::loop() { if (scheduleItems_->empty()) { return; } + // do not execute any command in the first 60 secondes if (uuid::get_uptime_sec() < 60) { return; @@ -395,6 +411,7 @@ void WebSchedulerService::loop() { scheduleItem.retry_cnt = runScheduleCommand(scheduleItem) ? 0xFF : 0; } } + last_tm_min = -1; } @@ -406,11 +423,13 @@ void WebSchedulerService::loop() { && scheduleItem.retry_cnt < MAX_STARTUP_RETRIES) { scheduleItem.retry_cnt = runScheduleCommand(scheduleItem) ? 0xFF : scheduleItem.retry_cnt + 1; } + if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min > 0 && (uptime_min % scheduleItem.elapsed_min == 0)) { runScheduleCommand(scheduleItem); } } + last_uptime_min = uptime_min; } @@ -427,6 +446,7 @@ void WebSchedulerService::loop() { runScheduleCommand(scheduleItem); } } + last_tm_min = tm->tm_min; } } diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index 4e04b0e7b..ebf584713 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -70,6 +70,7 @@ class WebSchedulerService : public StatefulService { void ha_reset() { ha_configdone_ = false; } + uint8_t count_entities(); bool onChange(const char * cmd); @@ -96,4 +97,4 @@ class WebSchedulerService : public StatefulService { } // namespace emsesp -#endif \ No newline at end of file +#endif diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index 6a4e099f0..b0e07af77 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -149,6 +149,7 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { if (settings.eth_power != -1) { // Ethernet Power -1 means disabled EMSESP::system_.remove_gpio(settings.eth_power, true); } + // remove the ethernet pins from valid list, regardless of whether the GPIOs are valid or not EMSESP::system_.remove_gpio(23, true); // MDC EMSESP::system_.remove_gpio(18, true); // MDIO @@ -410,6 +411,7 @@ void WebSettingsService::onUpdate() { if (WebSettings::has_flags(WebSettings::ChangeFlags::MODBUS)) { EMSESP::system_.modbus_init(); } + WebSettings::reset_flags(); EMSESP::system_.reset_unused_gpios(); } @@ -514,6 +516,7 @@ void WebSettings::set_board_profile(WebSettings & settings) { settings.board_profile = "S32"; // ESP32 standard WiFi with PSRAM } } + // override if we know the target from the build config like C3, S2, S3 etc.. #elif CONFIG_IDF_TARGET_ESP32C3 settings.board_profile = "C3MINI"; @@ -561,6 +564,7 @@ bool WebSettings::check_flag(int prev_v, int new_v, uint16_t flag) { #endif return true; } + return false; } diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index 681a965ee..e51dadca9 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -69,6 +69,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { if (esp_sntp_enabled()) { ntp_status = (EMSESP::system_.ntp_connected()) ? 2 : 1; } + root["ntp_status"] = ntp_status; if (ntp_status == 2) { // send back actual time if NTP enabled and active @@ -129,6 +130,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { root["psram_size"] = EMSESP::system_.PSram(); root["free_psram"] = ESP.getFreePsram() / 1024; } + root["model"] = EMSESP::system_.getBBQKeesGatewayDetails(); root["board"] = EMSESP::system_.getBBQKeesGatewayDetails(FUSE_VALUE::BOARD); #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 @@ -140,6 +142,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { if (partition != NULL) { root["recovery_version"] = EMSESP::system_.recovery_version(); } + partition = esp_ota_get_next_update_partition(nullptr); if (partition) { uint64_t buffer; @@ -159,6 +162,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { if (partition.second.version.empty() || partition.second.size == 0) { continue; } + JsonObject part = partitions.add(); part["partition"] = partition.first; part["version"] = partition.second.version; @@ -182,6 +186,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { // we're ready to do the actual restart ASAP EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); } + if (EMSESP::system_.systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD) { // error is reported, back to normal state EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_NORMAL); @@ -375,6 +380,7 @@ void WebStatusService::getVersions(JsonObject root) { if (info.version.empty()) { return; } + JsonObject out = root[key].to(); out["version"] = info.version; out["date"] = info.date; @@ -405,6 +411,7 @@ void WebStatusService::schedule_versions_refresh() { if (next == 0) { next = 1; // 0 is the "idle" sentinel — never let the wrap land there } + versions_next_fetch_ms_ = next; } @@ -444,6 +451,7 @@ void WebStatusService::loop() { if (versions_failures_ < VERSIONS_MAX_BACKOFF_STEPS) { versions_failures_++; } + interval = VERSIONS_RETRY_INTERVAL_MS << (versions_failures_ - 1); } @@ -451,6 +459,7 @@ void WebStatusService::loop() { if (next == 0) { next = 1; } + versions_next_fetch_ms_ = next; #endif } @@ -467,6 +476,7 @@ bool WebStatusService::refresh_versions_cache() { EMSESP::logger().warning("Unable to retrieve online version information (HTTP error code %d)", http_code); return false; } + DeserializationError err = deserializeJson(doc, result); if (err) { #if defined(EMSESP_DEBUG) @@ -483,6 +493,7 @@ bool WebStatusService::refresh_versions_cache() { out = {}; return; } + out.version = section["version"] | ""; out.date = section["date"] | ""; out.upgradeable = !out.version.empty() && FirmwareVersion(out.version) > current_version; @@ -516,6 +527,7 @@ bool WebStatusService::current_upgradeable() const { if (!versions_cache_valid_) { return false; } + return is_dev ? versions_dev_.upgradeable : versions_stable_.upgradeable; } diff --git a/src/web/WebStatusService.h b/src/web/WebStatusService.h index 935a54b6f..a570ebb91 100644 --- a/src/web/WebStatusService.h +++ b/src/web/WebStatusService.h @@ -18,6 +18,7 @@ class WebStatusService { void set_current_version(const std::string & version) { current_version_s = version; } + std::string get_current_version() { return current_version_s; } @@ -67,6 +68,7 @@ class WebStatusService { std::string date; bool upgradeable = false; }; + VersionInfo versions_stable_; VersionInfo versions_dev_; bool versions_cache_valid_ = false; // true once we've had at least one successful fetch