diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 55beed812f..1e852f6a96 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -136,7 +136,7 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont #ifdef USE_WATER_HEATER bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) { - // Water heater web_server support not yet implemented - this stub acknowledges the entity + this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::water_heater_all_json_generator); return true; } #endif diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index cab177c182..12115083f6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -28,6 +28,10 @@ #include "esphome/components/climate/climate.h" #endif +#ifdef USE_WATER_HEATER +#include "esphome/components/water_heater/water_heater.h" +#endif + #ifdef USE_WEBSERVER_LOCAL #if USE_WEBSERVER_VERSION == 2 #include "server_index_v2.h" @@ -558,7 +562,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { - auto *param = request->getParam("detail"); + auto *param = request->getParam(ESPHOME_F("detail")); return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE; } @@ -837,10 +841,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } auto call = is_on ? obj->turn_on() : obj->turn_off(); - parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed); + parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed); - if (request->hasParam("oscillation")) { - auto speed = request->getParam("oscillation")->value(); + if (request->hasParam(ESPHOME_F("oscillation"))) { + auto speed = request->getParam(ESPHOME_F("oscillation"))->value(); auto val = parse_on_off(speed.c_str()); switch (val) { case PARSE_ON: @@ -920,20 +924,20 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa if (is_on) { // Parse color parameters - parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f); - parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f); - parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f); - parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f); - parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f); - parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature); + parse_light_param_(request, ESPHOME_F("brightness"), call, &decltype(call)::set_brightness, 255.0f); + parse_light_param_(request, ESPHOME_F("r"), call, &decltype(call)::set_red, 255.0f); + parse_light_param_(request, ESPHOME_F("g"), call, &decltype(call)::set_green, 255.0f); + parse_light_param_(request, ESPHOME_F("b"), call, &decltype(call)::set_blue, 255.0f); + parse_light_param_(request, ESPHOME_F("white_value"), call, &decltype(call)::set_white, 255.0f); + parse_light_param_(request, ESPHOME_F("color_temp"), call, &decltype(call)::set_color_temperature); // Parse timing parameters - parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000); + parse_light_param_uint_(request, ESPHOME_F("flash"), call, &decltype(call)::set_flash_length, 1000); } - parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000); + parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000); if (is_on) { - parse_string_param_(request, "effect", call, &decltype(call)::set_effect); + parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect); } this->defer([call]() mutable { call.perform(); }); @@ -1016,14 +1020,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if ((request->hasParam("position") && !traits.get_supports_position()) || - (request->hasParam("tilt") && !traits.get_supports_tilt())) { + if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) || + (request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) { request->send(409); return; } - parse_float_param_(request, "position", call, &decltype(call)::set_position); - parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt); + parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); + parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1082,7 +1086,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_float_param_(request, "value", call, &decltype(call)::set_value); + parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1150,12 +1154,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam("value")) { + if (!request->hasParam(ESPHOME_F("value"))) { request->send(409); return; } - parse_string_param_(request, "value", call, &decltype(call)::set_date); + parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1214,12 +1218,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat auto call = obj->make_call(); - if (!request->hasParam("value")) { + if (!request->hasParam(ESPHOME_F("value"))) { request->send(409); return; } - parse_string_param_(request, "value", call, &decltype(call)::set_time); + parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1277,12 +1281,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur auto call = obj->make_call(); - if (!request->hasParam("value")) { + if (!request->hasParam(ESPHOME_F("value"))) { request->send(409); return; } - parse_string_param_(request, "value", call, &decltype(call)::set_datetime); + parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1342,7 +1346,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat } auto call = obj->make_call(); - parse_string_param_(request, "value", call, &decltype(call)::set_value); + parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1400,7 +1404,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } auto call = obj->make_call(); - parse_string_param_(request, "option", call, &decltype(call)::set_option); + parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1460,14 +1464,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url auto call = obj->make_call(); // Parse string mode parameters - parse_string_param_(request, "mode", call, &decltype(call)::set_mode); - parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode); - parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode); + parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode); + parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode); + parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode); // Parse temperature parameters - parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high); - parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low); - parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature); + parse_float_param_(request, ESPHOME_F("target_temperature_high"), call, + &decltype(call)::set_target_temperature_high); + parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low); + parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1706,12 +1711,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa } auto traits = obj->get_traits(); - if (request->hasParam("position") && !traits.get_supports_position()) { + if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) { request->send(409); return; } - parse_float_param_(request, "position", call, &decltype(call)::set_position); + parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position); this->defer([call]() mutable { call.perform(); }); request->send(200); @@ -1764,7 +1769,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques } auto call = obj->make_call(); - parse_string_param_(request, "code", call, &decltype(call)::set_code); + parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code); // Lookup table for alarm control panel methods static const struct { @@ -1825,6 +1830,117 @@ std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmContr } #endif +#ifdef USE_WATER_HEATER +void WebServer::on_water_heater_update(water_heater::WaterHeater *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; + this->events_.deferrable_send_state(obj, "state", water_heater_state_json_generator); +} +void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (water_heater::WaterHeater *obj : App.get_water_heaters()) { + auto entity_match = match.match_entity(obj); + if (!entity_match.matched) + continue; + + if (request->method() == HTTP_GET && entity_match.action_is_empty) { + auto detail = get_request_detail(request); + std::string data = this->water_heater_json_(obj, detail); + request->send(200, "application/json", data.c_str()); + return; + } + if (!match.method_equals("set")) { + request->send(404); + return; + } + auto call = obj->make_call(); + // Use base class reference for template deduction (make_call returns WaterHeaterCallInternal) + water_heater::WaterHeaterCall &base_call = call; + + // Parse mode parameter + parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode); + + // Parse temperature parameters + parse_float_param_(request, ESPHOME_F("target_temperature"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature); + parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_low); + parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call, + &water_heater::WaterHeaterCall::set_target_temperature_high); + + // Parse away mode parameter + parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away); + + // Parse on/off parameter + parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on); + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} + +std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) { + return web_server->water_heater_json_(static_cast(source), DETAIL_STATE); +} +std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + return web_server->water_heater_json_(static_cast(source), DETAIL_ALL); +} +std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) { + json::JsonBuilder builder; + JsonObject root = builder.root(); + char buf[PSTR_LOCAL_SIZE]; + + const auto mode = obj->get_mode(); + const char *mode_s = PSTR_LOCAL(water_heater::water_heater_mode_to_string(mode)); + + set_json_icon_state_value(root, obj, "water_heater", mode_s, mode, start_config); + + auto traits = obj->get_traits(); + + if (start_config == DETAIL_ALL) { + JsonArray modes = root[ESPHOME_F("modes")].to(); + for (auto m : traits.get_supported_modes()) + modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m))); + this->add_sorting_info_(root, obj); + } + + if (traits.get_supports_current_temperature()) { + float current = obj->get_current_temperature(); + if (!std::isnan(current)) + root[ESPHOME_F("current_temperature")] = current; + } + + if (traits.get_supports_two_point_target_temperature()) { + float low = obj->get_target_temperature_low(); + float high = obj->get_target_temperature_high(); + if (!std::isnan(low)) + root[ESPHOME_F("target_temperature_low")] = low; + if (!std::isnan(high)) + root[ESPHOME_F("target_temperature_high")] = high; + } else { + float target = obj->get_target_temperature(); + if (!std::isnan(target)) + root[ESPHOME_F("target_temperature")] = target; + } + + root[ESPHOME_F("min_temperature")] = traits.get_min_temperature(); + root[ESPHOME_F("max_temperature")] = traits.get_max_temperature(); + root[ESPHOME_F("step")] = traits.get_target_temperature_step(); + + if (traits.get_supports_away_mode()) { + root[ESPHOME_F("away")] = obj->is_away(); + } + + if (traits.has_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF)) { + root[ESPHOME_F("is_on")] = obj->is_on(); + } + + return builder.serialize(); +} +#endif + #ifdef USE_EVENT void WebServer::on_event(event::Event *obj) { if (!this->include_internal_ && obj->is_internal()) @@ -2060,6 +2176,9 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const { #endif #ifdef USE_UPDATE "update", +#endif +#ifdef USE_WATER_HEATER + "water_heater", #endif }; @@ -2220,6 +2339,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { else if (match.domain_equals("update")) { this->handle_update_request(request, match); } +#endif +#ifdef USE_WATER_HEATER + else if (match.domain_equals("water_heater")) { + this->handle_water_heater_request(request, match); + } #endif else { // No matching handler found - send 404 diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3e1dd867c6..c52cf981e0 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -35,6 +35,13 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE; namespace esphome::web_server { +// Type for parameter names that can be stored in flash on ESP8266 +#ifdef USE_ESP8266 +using ParamNameType = const __FlashStringHelper *; +#else +using ParamNameType = const char *; +#endif + /// Result of matching a URL against an entity struct EntityMatchResult { bool matched; ///< True if entity matched the URL @@ -429,6 +436,16 @@ class WebServer : public Controller, static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source); #endif +#ifdef USE_WATER_HEATER + void on_water_heater_update(water_heater::WaterHeater *obj) override; + + /// Handle a water_heater request under '/water_heater//'. + void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match); + + static std::string water_heater_state_json_generator(WebServer *web_server, void *source); + static std::string water_heater_all_json_generator(WebServer *web_server, void *source); +#endif + #ifdef USE_EVENT void on_event(event::Event *obj) override; @@ -472,7 +489,7 @@ class WebServer : public Controller, #ifdef USE_LIGHT // Helper to parse and apply a float parameter with optional scaling template - void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float), + void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float), float scale = 1.0f) { if (request->hasParam(param_name)) { auto value = parse_number(request->getParam(param_name)->value().c_str()); @@ -484,7 +501,7 @@ class WebServer : public Controller, // Helper to parse and apply a uint32_t parameter with optional scaling template - void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call, + void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(uint32_t), uint32_t scale = 1) { if (request->hasParam(param_name)) { auto value = parse_number(request->getParam(param_name)->value().c_str()); @@ -497,7 +514,7 @@ class WebServer : public Controller, // Generic helper to parse and apply a float parameter template - void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) { + void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) { if (request->hasParam(param_name)) { auto value = parse_number(request->getParam(param_name)->value().c_str()); if (value.has_value()) { @@ -508,7 +525,7 @@ class WebServer : public Controller, // Generic helper to parse and apply an int parameter template - void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) { + void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) { if (request->hasParam(param_name)) { auto value = parse_number(request->getParam(param_name)->value().c_str()); if (value.has_value()) { @@ -519,7 +536,7 @@ class WebServer : public Controller, // Generic helper to parse and apply a string parameter template - void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call, + void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(const std::string &)) { if (request->hasParam(param_name)) { // .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string @@ -528,6 +545,28 @@ class WebServer : public Controller, } } + // Generic helper to parse and apply a bool parameter + // Accepts: "on", "true", "1" (case-insensitive) as true + // Accepts: "off", "false", "0" (case-insensitive) as false + // Invalid values are ignored (setter not called) + template + void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) { + if (request->hasParam(param_name)) { + auto param_value = request->getParam(param_name)->value(); + // First check on/off (default), then true/false (custom) + auto val = parse_on_off(param_value.c_str()); + if (val == PARSE_NONE) { + val = parse_on_off(param_value.c_str(), "true", "false"); + } + if (val == PARSE_ON || param_value == "1") { + (call.*setter)(true); + } else if (val == PARSE_OFF || param_value == "0") { + (call.*setter)(false); + } + // PARSE_NONE/PARSE_TOGGLE: ignore invalid values + } + } + web_server_base::WebServerBase *base_; #ifdef USE_ESP32 AsyncEventSource events_{"/events", this}; @@ -606,6 +645,9 @@ class WebServer : public Controller, #ifdef USE_EVENT std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config); #endif +#ifdef USE_WATER_HEATER + std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config); +#endif #ifdef USE_UPDATE std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config); #endif diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index a21e9cb9ff..c3fe6f6780 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -232,6 +232,13 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif +#ifdef USE_WATER_HEATER + for (auto *obj : App.get_water_heaters()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "water_heater", ""); + } +#endif + stream->print(ESPHOME_F("

See ESPHome Web API for " "REST API documentation.

")); #if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)