Take what a tile can do, its tap actions, words and icons from Home Assistant (0.2.67)
GitHub issue #7 asked for cover.toggle on tap. ESP Screens now asks Home Assistant which actions each entity has instead of keeping lists: the editor offers On / off (covers included), sliders, controls and displays only where Home Assistant has the action, and refuses only new unsupported settings. Perform action runs any action Home Assistant offers for the tile's entity, under its own names and fields; the screen sends it with a call id and says Refused when Home Assistant refuses. Sensors round by display precision, and tiles, the top bar, timelines, vacuum chips and the inspector use Home Assistant's words where they showed raw states. Tiles show Home Assistant's icons, picked as its frontend does (the fonts grow to 309 glyphs). Existing screens keep every setting and tap as before: stored layouts load leniently, legacy taps route the same (1020 combinations), and messages are byte-identical without Home Assistant's words and icons. Firmware 0.2.58. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
@@ -27,6 +28,15 @@ inline bool valid_entity(const std::string &entity) {
|
||||
if (domain == allowed) return true;
|
||||
return false;
|
||||
}
|
||||
// An action as Home Assistant names it (domain.action: lowercase letters, digits, underscores), of any integration.
|
||||
inline bool valid_action(const std::string &action) {
|
||||
if (action.size() > 64) return false;
|
||||
auto dot = action.find('.');
|
||||
if (dot == std::string::npos || dot == 0 || dot + 1 == action.size() || action.find('.', dot + 1) != std::string::npos) return false;
|
||||
for (size_t i = 0; i < action.size(); ++i)
|
||||
if (i != dot && !(action[i] >= 'a' && action[i] <= 'z') && !(action[i] >= '0' && action[i] <= '9') && action[i] != '_') return false;
|
||||
return true;
|
||||
}
|
||||
// A tile keeps a fingerprint (FNV-1a) of its last state message instead of a copy of it. It is also an
|
||||
// ArduinoJson writer: the firmware hashes the attributes while serializing them, without a string.
|
||||
struct Fingerprint {
|
||||
@@ -79,13 +89,20 @@ struct Extra {
|
||||
bool charging = false;
|
||||
// Cover (firmware 0.2.50+): the tilt of its slats, 0 closed to 100 open.
|
||||
float tilt = NAN;
|
||||
// A tap that performs a Home Assistant action of the tile's own choosing (firmware 0.2.58+): the action, its data as
|
||||
// text, and the values Home Assistant renders itself (numbers, lists, true or false) as templates.
|
||||
std::string action;
|
||||
std::vector<std::pair<std::string, std::string>> action_data, action_templates;
|
||||
// Home Assistant's word for the state where the screen has none of its own (app 0.2.67+): "Open", "Playing", "Rinsing".
|
||||
std::string state_word;
|
||||
Choice *choice(char kind) { for (auto &c : choices) if (c.kind == kind) return &c; return nullptr; }
|
||||
bool empty() const {
|
||||
return hvac_modes.empty() && fan_modes.empty() && swing_modes.empty() && fan_mode.empty() && swing_mode.empty() &&
|
||||
hvac_action.empty() && options.empty() && forecast.empty() && hours.empty() && std::isnan(wind) &&
|
||||
std::isnan(feels) && wind_unit.empty() && sunrise.empty() && sunset.empty() && duration.empty() &&
|
||||
remaining.empty() && !timer_end && media_title.empty() && fan_speeds.empty() && fan_speed.empty() &&
|
||||
choices.empty() && room.empty() && !charging && std::isnan(tilt);
|
||||
choices.empty() && room.empty() && !charging && std::isnan(tilt) && action.empty() && action_data.empty() &&
|
||||
action_templates.empty() && state_word.empty();
|
||||
}
|
||||
};
|
||||
// The Extra of a tile on the heap, copied along with the tile like an ordinary member.
|
||||
@@ -128,6 +145,8 @@ struct Tile {
|
||||
uint32_t revision = 0, pending_revision = 0; // state_revision() fingerprints
|
||||
uint32_t pending_since = 0;
|
||||
bool pending = false, confirmed = false, local_feedback = false;
|
||||
// When Home Assistant refused the action a tap sent (firmware 0.2.58+); the tile says so for a moment.
|
||||
uint32_t refused_at = 0;
|
||||
ExtraBox extra_box;
|
||||
const Extra &extra() const { static const Extra none; return extra_box.ptr ? *extra_box.ptr : none; }
|
||||
Extra *extra_ptr() { return extra_box.ptr.get(); }
|
||||
@@ -151,7 +170,14 @@ struct Tile {
|
||||
// Two built-in cards, and only one of them is a clock that has to be redrawn every minute.
|
||||
bool is_clock() const { return entity == "screen.clock"; }
|
||||
bool is_settings() const { return entity == "screen.settings"; }
|
||||
bool available() const { return builtin() || (received && state != "unknown" && state != "unavailable" && !state.empty()); }
|
||||
// A scene, button or input button that never ran is "unknown" in Home Assistant, which still lets you press it
|
||||
// (hui-button-entity-row disables only an unavailable one): its state is the moment it last ran (firmware 0.2.58+).
|
||||
bool available() const {
|
||||
if (builtin()) return true;
|
||||
if (!received || state.empty() || state == "unavailable") return false;
|
||||
auto d = domain();
|
||||
return state != "unknown" || d == "scene" || d == "button" || d == "input_button";
|
||||
}
|
||||
bool active() const {
|
||||
return state == "on" || state == "cleaning" || state == "active" || (domain() == "person" && state == "home") ||
|
||||
(domain() == "sun" && state == "above_horizon") || (domain() == "climate" && available() && state != "off");
|
||||
|
||||
@@ -485,6 +485,24 @@ inline std::string receive(const std::string &payload) {
|
||||
// carry is collected in `next` and replaces the tile's Extra at the end (see Tile::set_extra).
|
||||
auto extra = root["x"];
|
||||
Extra next;
|
||||
// A tap's own Home Assistant action (app 0.2.67+): {"s": action, "d": [[key, text]], "t": [[key, template]]}.
|
||||
auto act = options["act"];
|
||||
if (act.is<JsonObject>()) {
|
||||
std::string service = string(act["s"], 64);
|
||||
auto pairs = [](JsonVariant list, std::vector<std::pair<std::string, std::string>> &out) {
|
||||
if (!list.is<JsonArray>()) return;
|
||||
for (JsonVariant pair : list.as<JsonArray>()) {
|
||||
if (out.size() == 8 || !pair.is<JsonArray>() || pair.as<JsonArray>().size() != 2) continue;
|
||||
std::string key = string(pair[0], 32);
|
||||
if (!key.empty()) out.emplace_back(std::move(key), string(pair[1], 400));
|
||||
}
|
||||
};
|
||||
if (valid_action(service)) {
|
||||
next.action = std::move(service);
|
||||
pairs(act["d"], next.action_data);
|
||||
pairs(act["t"], next.action_templates);
|
||||
}
|
||||
}
|
||||
if (extra["days"].is<JsonArray>()) for (JsonVariant day : extra["days"].as<JsonArray>()) {
|
||||
if (next.forecast.size() == 5) break;
|
||||
next.forecast.emplace_back(); auto &f = next.forecast.back();
|
||||
@@ -573,6 +591,7 @@ inline std::string receive(const std::string &payload) {
|
||||
if (!std::isfinite(tile.battery)) tile.battery = number(extra["bat"]);
|
||||
next.charging = extra["chg"].is<int>() && extra["chg"].as<int>() == 1;
|
||||
next.room = string(extra["room"], 32);
|
||||
next.state_word = string(extra["w"], 32);
|
||||
tile.set_extra(std::move(next));
|
||||
// Home Assistant reports the edited value: the -/+ pill follows its state again.
|
||||
if(std::isfinite(tile.edit_value) && tile.edit_sent && std::fabs(tile_controls::edit_target(tile)-tile.edit_value)<0.051f)tile.edit_value=NAN;
|
||||
@@ -588,7 +607,51 @@ inline std::string receive(const std::string &payload) {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
inline void action(const std::string &service, const std::string &entity, const std::string &key="", const std::string &value="") {
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
// Home Assistant answers an action sent with a call id (firmware 0.2.58+, Home Assistant 2025.10+). ESPHome keeps each
|
||||
// answer's callback until the answer arrives and has no timeout of its own, so a tap asks for at most four answers at a
|
||||
// time and ends the ones Home Assistant never gives (actions not allowed, an older Home Assistant) after eight seconds.
|
||||
// Only a refusal shows: the tile says Refused for a moment instead of waiting.
|
||||
struct WatchedCall { uint32_t id = 0, since = 0; };
|
||||
inline std::array<WatchedCall, 4> watched_calls{};
|
||||
inline const char *const NO_ANSWER = "no answer";
|
||||
inline void watch_call(esphome::api::HomeassistantActionRequest &request, const std::string &entity) {
|
||||
static uint32_t last_id = 0x5C000000u; // apart from ESPHome's own counter for YAML actions, which starts at 1
|
||||
auto slot = std::find_if(watched_calls.begin(), watched_calls.end(), [](const WatchedCall &c) { return c.id == 0; });
|
||||
if (slot == watched_calls.end()) return;
|
||||
const uint32_t id = ++last_id;
|
||||
*slot = {id, esphome::millis()};
|
||||
request.call_id = id;
|
||||
esphome::api::global_api_server->register_action_response_callback(id, [id, entity](const esphome::api::ActionResponse &answer) {
|
||||
for (auto &c : watched_calls) if (c.id == id) c = {};
|
||||
if (answer.is_success() || answer.get_error_message().c_str() == NO_ANSWER) return;
|
||||
ESP_LOGW("runtime_action", "Home Assistant refused the action for %s: %.*s", entity.c_str(),
|
||||
(int) answer.get_error_message().size(), answer.get_error_message().c_str());
|
||||
for (size_t i = 0; i < model.tiles.size(); ++i) if (model.tiles[i].entity == entity) {
|
||||
model.tiles[i].pending = false;
|
||||
model.tiles[i].refused_at = std::max<uint32_t>(1, esphome::millis());
|
||||
refresh_tile(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
inline void expire_calls(uint32_t now) {
|
||||
for (auto &c : watched_calls) {
|
||||
if (!c.id || now - c.since < 8000) continue;
|
||||
const uint32_t id = c.id;
|
||||
c = {};
|
||||
esphome::api::global_api_server->handle_action_response(id, false, esphome::StringRef(NO_ANSWER, 9));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Marks every tile of the entity busy and sends; `watch` asks Home Assistant for an answer (a tap).
|
||||
inline void send_action(esphome::api::HomeassistantActionRequest &request, const std::string &entity, bool watch) {
|
||||
for(size_t i=0;i<model.tiles.size();++i) if(model.tiles[i].entity==entity){model.tiles[i].begin(esphome::millis());model.tiles[i].refused_at=0;refresh_tile(i);}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
if (watch) watch_call(request, entity);
|
||||
#endif
|
||||
esphome::api::global_api_server->send_homeassistant_action(request);
|
||||
}
|
||||
inline void action(const std::string &service, const std::string &entity, const std::string &key="", const std::string &value="", bool watch=false) {
|
||||
if (!fresh() || !valid_entity(entity)) return;
|
||||
esphome::api::HomeassistantActionRequest request;
|
||||
request.service = esphome::StringRef(service);
|
||||
@@ -598,10 +661,38 @@ inline void action(const std::string &service, const std::string &entity, const
|
||||
entry.value = esphome::StringRef(entity);
|
||||
request.data.push_back(entry);
|
||||
if(!key.empty()) {esphome::api::HomeassistantServiceMap param;param.key=esphome::StringRef(key);param.value=esphome::StringRef(value);request.data.push_back(param);}
|
||||
for(size_t i=0;i<model.tiles.size();++i) if(model.tiles[i].entity==entity){model.tiles[i].begin(esphome::millis());refresh_tile(i);}
|
||||
esphome::api::global_api_server->send_homeassistant_action(request);
|
||||
send_action(request, entity, watch);
|
||||
ESP_LOGI("runtime_action","Sent service=%s entity=%s",service.c_str(),entity.c_str());
|
||||
}
|
||||
// A tap's own action (firmware 0.2.58+): the tile's entity with the data the app sent, text as data and the values Home
|
||||
// Assistant renders itself (numbers, lists, true or false) as a data_template.
|
||||
inline void perform(const Tile &tile) {
|
||||
const auto &x = tile.extra();
|
||||
if (!fresh() || !valid_entity(tile.entity) || x.action.empty()) return;
|
||||
esphome::api::HomeassistantActionRequest request;
|
||||
request.service = esphome::StringRef(x.action);
|
||||
request.data.init(1 + x.action_data.size());
|
||||
esphome::api::HomeassistantServiceMap target;
|
||||
target.key = esphome::StringRef("entity_id");
|
||||
target.value = esphome::StringRef(tile.entity);
|
||||
request.data.push_back(target);
|
||||
for (const auto &pair : x.action_data) {
|
||||
esphome::api::HomeassistantServiceMap entry;
|
||||
entry.key = esphome::StringRef(pair.first);
|
||||
entry.value = esphome::StringRef(pair.second);
|
||||
request.data.push_back(entry);
|
||||
}
|
||||
request.data_template.init(x.action_templates.size());
|
||||
for (const auto &pair : x.action_templates) {
|
||||
esphome::api::HomeassistantServiceMap entry;
|
||||
entry.key = esphome::StringRef(pair.first);
|
||||
entry.value = esphome::StringRef(pair.second);
|
||||
request.data_template.push_back(entry);
|
||||
}
|
||||
send_action(request, tile.entity, true);
|
||||
ESP_LOGI("runtime_action", "Sent service=%s entity=%s (%u values)", x.action.c_str(), tile.entity.c_str(),
|
||||
(unsigned) (x.action_data.size() + x.action_templates.size()));
|
||||
}
|
||||
// A detail card asks the manager for its history (app 0.2.59+ answers with op "history"). An event, like
|
||||
// setting_event: it needs no permission to call Home Assistant actions.
|
||||
inline void history_request(const std::string &entity, uint32_t hours) {
|
||||
@@ -664,6 +755,8 @@ inline std::string detail_state(const Tile &t){
|
||||
if(t.state=="idle")return "Idle";
|
||||
if(t.state=="error")return "Check the robot in HA";
|
||||
if(!t.available())return "Unavailable";
|
||||
// Home Assistant's word where the screen has none of its own (firmware 0.2.58+).
|
||||
if(!t.extra().state_word.empty())return t.extra().state_word;
|
||||
return t.state;
|
||||
}
|
||||
inline void hide_detail(){if(detail_root)lv_obj_add_flag(detail_root,LV_OBJ_FLAG_HIDDEN);}
|
||||
@@ -1767,25 +1860,28 @@ inline void event(lv_event_t *event) {
|
||||
if (!tile.available() || tile.loading(esphome::millis()) || tile.tap=="none") return;
|
||||
// A camera or an image entity opens full screen on a board that draws images (firmware 0.2.57+).
|
||||
if(d=="camera" || d=="image"){if(camera_supported())camera_open(tile.entity,tile.name);return;}
|
||||
bool open = code == LV_EVENT_LONG_PRESSED || d == "climate" || d == "vacuum" || d == "cover";
|
||||
if(code==LV_EVENT_SHORT_CLICKED && tile.tap=="detail")open=true;
|
||||
if(code==LV_EVENT_SHORT_CLICKED && tile.tap=="toggle")open=false;
|
||||
if(d=="media_player" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("media_player.toggle",tile.entity);return;}
|
||||
if(d=="climate" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("climate.toggle",tile.entity);return;}
|
||||
// A short tap runs or pauses the timer; holding opens the card with a cancel button.
|
||||
if(d=="timer" && !open){action(tile.state=="active"?"timer.pause":"timer.start",tile.entity);return;}
|
||||
if(d=="sensor" || d=="binary_sensor" || d=="weather" || d=="number" || d=="input_number" || d=="select" || d=="input_select" || d=="media_player" || d=="vacuum" || d=="cover" || d=="sun" || d=="person" || d=="timer") { tile.begin(esphome::millis(),true); active_index=w.index; show_detail(w.index); return; }
|
||||
if (open) {
|
||||
if (d == "light" || d == "climate" || d == "vacuum" || d == "fan") {
|
||||
// tile_controls::tap_route decides; tests/test_tile_controls.cpp keeps every older tap choice routed as before.
|
||||
auto tap = tile_controls::tap_route(tile, code == LV_EVENT_LONG_PRESSED);
|
||||
switch (tap.route) {
|
||||
case tile_controls::TapRoute::ACTION:
|
||||
action(tap.service, tile.entity, "", "", true);
|
||||
return;
|
||||
case tile_controls::TapRoute::CUSTOM:
|
||||
perform(tile);
|
||||
return;
|
||||
case tile_controls::TapRoute::CARD:
|
||||
if (tap.busy) tile.begin(esphome::millis(), true);
|
||||
active_index = w.index;
|
||||
tile.begin(esphome::millis(),true);
|
||||
show_detail(w.index);
|
||||
return;
|
||||
case tile_controls::TapRoute::OVERLAY:
|
||||
active_index = w.index;
|
||||
tile.begin(esphome::millis(), true);
|
||||
if (detail) detail(tile);
|
||||
} else { active_index=w.index;show_detail(w.index); }
|
||||
return;
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (d == "light" || d == "switch" || d == "input_boolean" || d == "fan") action(d + ".toggle", tile.entity);
|
||||
if (d == "scene" || d == "script") action(d + ".turn_on", tile.entity);
|
||||
if (d == "button" || d == "input_button") action(d + ".press", tile.entity);
|
||||
}
|
||||
// The same guard for any local style: a page switch mostly hands a slot the colours it already has,
|
||||
// and each real set refreshes the style and invalidates the object (about 0.3 ms on the Guition).
|
||||
@@ -2407,6 +2503,7 @@ inline void render_slot(size_t slot) {
|
||||
// Nothing in Home Assistant stands behind the settings card, so it says the same with the link down.
|
||||
if (t.is_settings()) value = "Tap to open";
|
||||
else if (!fresh() || !t.available()) value = "Unavailable";
|
||||
else if (t.refused_at && esphome::millis() - t.refused_at < 4000) value = "Refused";
|
||||
else if (d == "light" && t.state == "on" && std::isfinite(t.brightness)) value = std::to_string(static_cast<int>(std::lround(std::clamp(t.brightness, 0.0f, 255.0f) * 100 / 255))) + " %";
|
||||
else if (d == "climate" && std::isfinite(t.target)) { char b[32]; snprintf(b, sizeof(b), "%.1f°", t.target); value = b; }
|
||||
else if (d == "person") value = t.state=="home"?"Home":t.state=="not_home"?"Away":t.state;
|
||||
@@ -2420,6 +2517,8 @@ inline void render_slot(size_t slot) {
|
||||
else if (value == "off") value = "Off";
|
||||
else if (value == "cleaning") value = "Cleaning";
|
||||
else if (value == "docked") value = "Docked";
|
||||
// Home Assistant's word where the screen has none of its own (firmware 0.2.58+): a cover says Open, a washer Rinsing.
|
||||
else if (!t.extra().state_word.empty()) value = t.extra().state_word;
|
||||
else if (!t.unit.empty() && !watch) value += " " + t.unit;
|
||||
bool pending=t.loading(esphome::millis());
|
||||
if(d=="weather" && std::isfinite(t.current)) {char b[32];snprintf(b,sizeof(b),"%.1f %s",t.current,t.unit.c_str());value=b;if(watch){snprintf(b,sizeof(b),"%.1f",t.current);value=b;}}
|
||||
@@ -3054,8 +3153,13 @@ inline void tick() {
|
||||
// HA dropping or returning and the feed timing out change every card at once.
|
||||
bool now_fresh=fresh();
|
||||
if(now_fresh!=was_fresh){was_fresh=now_fresh;dirty_all=true;redraw=true;}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
expire_calls(esphome::millis());
|
||||
#endif
|
||||
for(size_t i=0;i<model.tiles.size();++i){
|
||||
auto &t=model.tiles[i];if(!t.pending || t.loading(esphome::millis()))continue;
|
||||
auto &t=model.tiles[i];
|
||||
if(t.refused_at && esphome::millis()-t.refused_at>=4000){t.refused_at=0;card(i);}
|
||||
if(!t.pending || t.loading(esphome::millis()))continue;
|
||||
t.pending=false;card(i);
|
||||
// A vacuum chip Home Assistant never confirmed goes back to what the robot reports.
|
||||
bool sent=false;if(auto *x=t.extra_ptr())for(auto &c:x->choices){sent=sent||!c.sent.empty();c.sent.clear();}
|
||||
|
||||
@@ -10,6 +10,9 @@ DashboardInbox = ns.class_("DashboardInbox", text.Text, cg.Component)
|
||||
CONFIG_SCHEMA = text.text_schema(DashboardInbox, mode="TEXT", entity_category="config").extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
async def to_code(config):
|
||||
# A tap asks Home Assistant whether it took the action (firmware 0.2.58+): ESPHome's own action responses, which it
|
||||
# otherwise only compiles in for a YAML homeassistant.action with on_success or on_error.
|
||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await text.register_text(var, config, min_length=0, max_length=255, pattern=None)
|
||||
|
||||
@@ -370,4 +370,34 @@ inline Action edit_action(const Tile &t, float value) {
|
||||
if (d == "number" || d == "input_number") return {d + ".set_value", "value", text};
|
||||
return {};
|
||||
}
|
||||
|
||||
// ---- What a finger on a tile does ----
|
||||
// The tile's own `tap` choice comes first. `toggle` sends <domain>.toggle on every domain (firmware 0.2.58+): the app
|
||||
// only offers it where Home Assistant lists that action for the entity, such as a cover, which stops while it moves.
|
||||
// Otherwise the domain decides, as before: holding or `detail` opens a card, a short tap switches, runs or presses.
|
||||
// CARD is the runtime detail card (show_detail), OVERLAY the board's own light, fan and climate card; `busy` marks the
|
||||
// tile busy for a moment while the card opens.
|
||||
// CUSTOM (firmware 0.2.58+) is an action of the tile's own choosing from Home Assistant's list, with its data.
|
||||
enum class TapRoute : uint8_t { NONE, ACTION, CARD, OVERLAY, CUSTOM };
|
||||
struct Tap { TapRoute route = TapRoute::NONE; std::string service; bool busy = false; };
|
||||
inline bool runtime_card_domain(const std::string &d) {
|
||||
return d == "sensor" || d == "binary_sensor" || d == "weather" || d == "number" || d == "input_number" || d == "select" ||
|
||||
d == "input_select" || d == "media_player" || d == "vacuum" || d == "cover" || d == "sun" || d == "person" || d == "timer";
|
||||
}
|
||||
inline Tap tap_route(const Tile &t, bool hold) {
|
||||
const std::string d = t.domain();
|
||||
if (t.tap == "none") return {};
|
||||
// A tile whose action didn't arrive (an app before 0.2.67) taps automatically, as older firmware does.
|
||||
if (!hold && t.tap == "action" && !t.extra().action.empty()) return {TapRoute::CUSTOM, t.extra().action};
|
||||
if (!hold && t.tap == "toggle") return {TapRoute::ACTION, d + ".toggle"};
|
||||
bool open = hold || t.tap == "detail" || d == "climate" || d == "vacuum" || d == "cover";
|
||||
// A short tap runs or pauses the timer; holding opens the card with a cancel button.
|
||||
if (d == "timer" && !open) return {TapRoute::ACTION, t.state == "active" ? "timer.pause" : "timer.start"};
|
||||
if (runtime_card_domain(d)) return {TapRoute::CARD, "", true};
|
||||
if (open) return d == "light" || d == "climate" || d == "fan" ? Tap{TapRoute::OVERLAY, "", true} : Tap{TapRoute::CARD, "", false};
|
||||
if (d == "light" || d == "switch" || d == "input_boolean" || d == "fan") return {TapRoute::ACTION, d + ".toggle"};
|
||||
if (d == "scene" || d == "script") return {TapRoute::ACTION, d + ".turn_on"};
|
||||
if (d == "button" || d == "input_button") return {TapRoute::ACTION, d + ".press"};
|
||||
return {};
|
||||
}
|
||||
} // namespace tile_controls
|
||||
|
||||
@@ -191,6 +191,131 @@ inline constexpr Named NAMES[] = {
|
||||
{"information-outline", 0xF02FD},
|
||||
{"restart", 0xF0709},
|
||||
{"lightbulb-off", 0xF0E4F},
|
||||
{"account-arrow-right", 0xF0B53},
|
||||
{"air-filter", 0xF0D43},
|
||||
{"air-humidifier", 0xF1099},
|
||||
{"air-humidifier-off", 0xF1466},
|
||||
{"alert-circle", 0xF0028},
|
||||
{"angle-acute", 0xF0937},
|
||||
{"arrow-bottom-left", 0xF0042},
|
||||
{"arrow-bottom-right", 0xF0043},
|
||||
{"arrow-down-box", 0xF06C0},
|
||||
{"arrow-left-right", 0xF0E73},
|
||||
{"arrow-right", 0xF0054},
|
||||
{"arrow-split-vertical", 0xF093C},
|
||||
{"arrow-top-left", 0xF005B},
|
||||
{"arrow-top-right", 0xF005C},
|
||||
{"arrow-up-box", 0xF06C3},
|
||||
{"audio-video", 0xF093D},
|
||||
{"audio-video-off", 0xF11B6},
|
||||
{"battery", 0xF0079},
|
||||
{"battery-10", 0xF007A},
|
||||
{"battery-20", 0xF007B},
|
||||
{"battery-30", 0xF007C},
|
||||
{"battery-40", 0xF007D},
|
||||
{"battery-50", 0xF007E},
|
||||
{"battery-60", 0xF007F},
|
||||
{"battery-70", 0xF0080},
|
||||
{"battery-80", 0xF0081},
|
||||
{"battery-90", 0xF0082},
|
||||
{"battery-alert", 0xF0083},
|
||||
{"battery-charging", 0xF0084},
|
||||
{"battery-outline", 0xF008E},
|
||||
{"battery-unknown", 0xF0091},
|
||||
{"blinds-horizontal", 0xF1A2B},
|
||||
{"blinds-horizontal-closed", 0xF1A2C},
|
||||
{"bluetooth", 0xF00AF},
|
||||
{"bluetooth-connect", 0xF00B1},
|
||||
{"brightness-5", 0xF00DE},
|
||||
{"brightness-7", 0xF00E0},
|
||||
{"button-pointer", 0xF1B50},
|
||||
{"car-battery", 0xF010C},
|
||||
{"car-coolant-level", 0xF1019},
|
||||
{"cash", 0xF0114},
|
||||
{"cast-connected", 0xF0119},
|
||||
{"cast-off", 0xF078A},
|
||||
{"check-circle", 0xF05E0},
|
||||
{"check-circle-outline", 0xF05E1},
|
||||
{"check-network-outline", 0xF0C54},
|
||||
{"checkbox-marked-circle", 0xF0133},
|
||||
{"circle-slice-8", 0xF0AA5},
|
||||
{"clock", 0xF0954},
|
||||
{"clock-start", 0xF0155},
|
||||
{"close-circle-outline", 0xF015A},
|
||||
{"close-network-outline", 0xF0C5F},
|
||||
{"compass-rose", 0xF1382},
|
||||
{"crop-portrait", 0xF01A1},
|
||||
{"crosshairs-question", 0xF1136},
|
||||
{"current-ac", 0xF1480},
|
||||
{"database", 0xF01BC},
|
||||
{"ear-hearing", 0xF07C5},
|
||||
{"eye", 0xF0208},
|
||||
{"eye-check", 0xF0D04},
|
||||
{"fan-off", 0xF081D},
|
||||
{"format-list-bulleted", 0xF0279},
|
||||
{"garage-open", 0xF06DA},
|
||||
{"gate", 0xF0299},
|
||||
{"gate-open", 0xF116A},
|
||||
{"home-outline", 0xF06A1},
|
||||
{"lan-connect", 0xF0318},
|
||||
{"lan-disconnect", 0xF0319},
|
||||
{"lightning-bolt", 0xF140B},
|
||||
{"lock-alert", 0xF08EE},
|
||||
{"lock-clock", 0xF097F},
|
||||
{"lock-open", 0xF033F},
|
||||
{"molecule", 0xF0BAC},
|
||||
{"molecule-co", 0xF12FE},
|
||||
{"motion-sensor-off", 0xF1435},
|
||||
{"music-note-off", 0xF038A},
|
||||
{"octagon", 0xF03C3},
|
||||
{"package", 0xF03D3},
|
||||
{"package-up", 0xF03D5},
|
||||
{"ph", 0xF17C5},
|
||||
{"pipe-valve", 0xF184D},
|
||||
{"power-plug-off", 0xF06A6},
|
||||
{"progress-clock", 0xF0996},
|
||||
{"projector-off", 0xF1A23},
|
||||
{"radioactive", 0xF043C},
|
||||
{"ray-vertex", 0xF0445},
|
||||
{"roller-shade", 0xF1A6B},
|
||||
{"script-text-play", 0xF1727},
|
||||
{"security", 0xF0483},
|
||||
{"shield", 0xF0498},
|
||||
{"shield-airplane", 0xF06BB},
|
||||
{"shield-lock", 0xF099D},
|
||||
{"shield-moon", 0xF1828},
|
||||
{"shield-off", 0xF099E},
|
||||
{"shield-outline", 0xF0499},
|
||||
{"sine-wave", 0xF095B},
|
||||
{"smoke-detector-alert", 0xF192E},
|
||||
{"smoke-detector-variant", 0xF180B},
|
||||
{"smoke-detector-variant-alert", 0xF1930},
|
||||
{"speaker-off", 0xF04C4},
|
||||
{"speaker-pause", 0xF1B73},
|
||||
{"speaker-play", 0xF1B72},
|
||||
{"speedometer", 0xF04C5},
|
||||
{"spoon-sugar", 0xF1429},
|
||||
{"sprout-outline", 0xF0E67},
|
||||
{"square", 0xF0764},
|
||||
{"square-outline", 0xF0763},
|
||||
{"storage-tank", 0xF1A75},
|
||||
{"sun-wireless", 0xF17FE},
|
||||
{"television-off", 0xF083B},
|
||||
{"television-pause", 0xF0F89},
|
||||
{"television-play", 0xF0ECF},
|
||||
{"texture-box", 0xF0FE6},
|
||||
{"thermometer-lines", 0xF0510},
|
||||
{"toggle-switch-variant", 0xF1A25},
|
||||
{"toggle-switch-variant-off", 0xF1A26},
|
||||
{"transmission-tower", 0xF0D3E},
|
||||
{"vibrate", 0xF0566},
|
||||
{"water-boiler-off", 0xF11B4},
|
||||
{"water-off", 0xF058D},
|
||||
{"water-opacity", 0xF1855},
|
||||
{"weight", 0xF05A1},
|
||||
{"wifi", 0xF05A9},
|
||||
{"window-closed", 0xF05AE},
|
||||
{"window-open", 0xF05B1},
|
||||
};
|
||||
inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);
|
||||
// Codepoint of a name the fonts carry; 0 for any other name.
|
||||
|
||||
Reference in New Issue
Block a user