mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 05:24:53 +01:00
dashboard: Add support for firing events (#5775)
* dashboard: fire events when entry is updated or state changes * dashboard: fire events when entry is updated or state changes * dashboard: fire events when entry is updated or state changes * tweaks * fixes * remove typing_extensions * rename for asyncio * rename for asyncio * rename for asyncio * preen * lint * lint * move dict converter * lint
This commit is contained in:
parent
288af1f4d2
commit
3c243e663f
8 changed files with 251 additions and 78 deletions
8
esphome/dashboard/const.py
Normal file
8
esphome/dashboard/const.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
EVENT_ENTRY_ADDED = "entry_added"
|
||||
EVENT_ENTRY_REMOVED = "entry_removed"
|
||||
EVENT_ENTRY_UPDATED = "entry_updated"
|
||||
EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
|
||||
|
||||
SENTINEL = object()
|
|
@ -3,7 +3,9 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from ..zeroconf import DiscoveredImport
|
||||
from .entries import DashboardEntries
|
||||
|
@ -12,16 +14,55 @@ from .settings import DashboardSettings
|
|||
if TYPE_CHECKING:
|
||||
from .status.mdns import MDNSStatus
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
"""Dashboard Event."""
|
||||
|
||||
event_type: str
|
||||
data: dict[str, Any]
|
||||
|
||||
|
||||
class EventBus:
|
||||
"""Dashboard event bus."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Dashboard event bus."""
|
||||
self._listeners: dict[str, set[Callable[[Event], None]]] = {}
|
||||
|
||||
def async_add_listener(
|
||||
self, event_type: str, listener: Callable[[Event], None]
|
||||
) -> Callable[[], None]:
|
||||
"""Add a listener to the event bus."""
|
||||
self._listeners.setdefault(event_type, set()).add(listener)
|
||||
return partial(self._async_remove_listener, event_type, listener)
|
||||
|
||||
def _async_remove_listener(
|
||||
self, event_type: str, listener: Callable[[Event], None]
|
||||
) -> None:
|
||||
"""Remove a listener from the event bus."""
|
||||
self._listeners[event_type].discard(listener)
|
||||
|
||||
def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None:
|
||||
"""Fire an event."""
|
||||
event = Event(event_type, event_data)
|
||||
|
||||
_LOGGER.debug("Firing event: %s", event)
|
||||
|
||||
for listener in self._listeners.get(event_type, set()):
|
||||
listener(event)
|
||||
|
||||
|
||||
class ESPHomeDashboard:
|
||||
"""Class that represents the dashboard."""
|
||||
|
||||
__slots__ = (
|
||||
"bus",
|
||||
"entries",
|
||||
"loop",
|
||||
"ping_result",
|
||||
"import_result",
|
||||
"stop_event",
|
||||
"ping_request",
|
||||
|
@ -32,9 +73,9 @@ class ESPHomeDashboard:
|
|||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the ESPHomeDashboard."""
|
||||
self.bus = EventBus()
|
||||
self.entries: DashboardEntries | None = None
|
||||
self.loop: asyncio.AbstractEventLoop | None = None
|
||||
self.ping_result: dict[str, bool | None] = {}
|
||||
self.import_result: dict[str, DiscoveredImport] = {}
|
||||
self.stop_event = threading.Event()
|
||||
self.ping_request: asyncio.Event | None = None
|
||||
|
@ -46,7 +87,7 @@ class ESPHomeDashboard:
|
|||
"""Setup the dashboard."""
|
||||
self.loop = asyncio.get_running_loop()
|
||||
self.ping_request = asyncio.Event()
|
||||
self.entries = DashboardEntries(self.settings.config_dir)
|
||||
self.entries = DashboardEntries(self)
|
||||
|
||||
async def async_run(self) -> None:
|
||||
"""Run the dashboard."""
|
||||
|
|
|
@ -3,24 +3,78 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from esphome import const, util
|
||||
from esphome.storage_json import StorageJSON, ext_storage_path
|
||||
|
||||
from .const import (
|
||||
EVENT_ENTRY_ADDED,
|
||||
EVENT_ENTRY_REMOVED,
|
||||
EVENT_ENTRY_STATE_CHANGED,
|
||||
EVENT_ENTRY_UPDATED,
|
||||
)
|
||||
from .enum import StrEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core import ESPHomeDashboard
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DashboardCacheKeyType = tuple[int, int, float, int]
|
||||
|
||||
# Currently EntryState is a simple
|
||||
# online/offline/unknown enum, but in the future
|
||||
# it may be expanded to include more states
|
||||
|
||||
|
||||
class EntryState(StrEnum):
|
||||
ONLINE = "online"
|
||||
OFFLINE = "offline"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
_BOOL_TO_ENTRY_STATE = {
|
||||
True: EntryState.ONLINE,
|
||||
False: EntryState.OFFLINE,
|
||||
None: EntryState.UNKNOWN,
|
||||
}
|
||||
_ENTRY_STATE_TO_BOOL = {
|
||||
EntryState.ONLINE: True,
|
||||
EntryState.OFFLINE: False,
|
||||
EntryState.UNKNOWN: None,
|
||||
}
|
||||
|
||||
|
||||
def bool_to_entry_state(value: bool) -> EntryState:
|
||||
"""Convert a bool to an entry state."""
|
||||
return _BOOL_TO_ENTRY_STATE[value]
|
||||
|
||||
|
||||
def entry_state_to_bool(value: EntryState) -> bool | None:
|
||||
"""Convert an entry state to a bool."""
|
||||
return _ENTRY_STATE_TO_BOOL[value]
|
||||
|
||||
|
||||
class DashboardEntries:
|
||||
"""Represents all dashboard entries."""
|
||||
|
||||
__slots__ = ("_loop", "_config_dir", "_entries", "_loaded_entries", "_update_lock")
|
||||
__slots__ = (
|
||||
"_dashboard",
|
||||
"_loop",
|
||||
"_config_dir",
|
||||
"_entries",
|
||||
"_entry_states",
|
||||
"_loaded_entries",
|
||||
"_update_lock",
|
||||
)
|
||||
|
||||
def __init__(self, config_dir: str) -> None:
|
||||
def __init__(self, dashboard: ESPHomeDashboard) -> None:
|
||||
"""Initialize the DashboardEntries."""
|
||||
self._dashboard = dashboard
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._config_dir = config_dir
|
||||
self._config_dir = dashboard.settings.config_dir
|
||||
# Entries are stored as
|
||||
# {
|
||||
# "path/to/file.yaml": DashboardEntry,
|
||||
|
@ -46,6 +100,25 @@ class DashboardEntries:
|
|||
"""Return all entries."""
|
||||
return list(self._entries.values())
|
||||
|
||||
def set_state(self, entry: DashboardEntry, state: EntryState) -> None:
|
||||
"""Set the state for an entry."""
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._async_set_state(entry, state), self._loop
|
||||
).result()
|
||||
|
||||
async def _async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
|
||||
"""Set the state for an entry."""
|
||||
self.async_set_state(entry, state)
|
||||
|
||||
def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
|
||||
"""Set the state for an entry."""
|
||||
if entry.state == state:
|
||||
return
|
||||
entry.state = state
|
||||
self._dashboard.bus.async_fire(
|
||||
EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state}
|
||||
)
|
||||
|
||||
async def async_request_update_entries(self) -> None:
|
||||
"""Request an update of the dashboard entries from disk.
|
||||
|
||||
|
@ -81,16 +154,17 @@ class DashboardEntries:
|
|||
path_to_cache_key = await self._loop.run_in_executor(
|
||||
None, self._get_path_to_cache_key
|
||||
)
|
||||
entries = self._entries
|
||||
added: dict[DashboardEntry, DashboardCacheKeyType] = {}
|
||||
updated: dict[DashboardEntry, DashboardCacheKeyType] = {}
|
||||
removed: set[DashboardEntry] = {
|
||||
entry
|
||||
for filename, entry in self._entries.items()
|
||||
for filename, entry in entries.items()
|
||||
if filename not in path_to_cache_key
|
||||
}
|
||||
entries = self._entries
|
||||
|
||||
for path, cache_key in path_to_cache_key.items():
|
||||
if entry := self._entries.get(path):
|
||||
if entry := entries.get(path):
|
||||
if entry.cache_key != cache_key:
|
||||
updated[entry] = cache_key
|
||||
else:
|
||||
|
@ -102,17 +176,17 @@ class DashboardEntries:
|
|||
None, self._load_entries, {**added, **updated}
|
||||
)
|
||||
|
||||
bus = self._dashboard.bus
|
||||
for entry in added:
|
||||
_LOGGER.debug("Added dashboard entry %s", entry.path)
|
||||
entries[entry.path] = entry
|
||||
bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry})
|
||||
|
||||
if entry in removed:
|
||||
_LOGGER.debug("Removed dashboard entry %s", entry.path)
|
||||
entries.pop(entry.path)
|
||||
for entry in removed:
|
||||
del entries[entry.path]
|
||||
bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry})
|
||||
|
||||
for entry in updated:
|
||||
_LOGGER.debug("Updated dashboard entry %s", entry.path)
|
||||
# In the future we can fire events when entries are added/removed/updated
|
||||
bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry})
|
||||
|
||||
def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]:
|
||||
"""Return a dict of path to cache key."""
|
||||
|
@ -152,29 +226,64 @@ class DashboardEntry:
|
|||
This class is thread-safe and read-only.
|
||||
"""
|
||||
|
||||
__slots__ = ("path", "filename", "_storage_path", "cache_key", "storage")
|
||||
__slots__ = (
|
||||
"path",
|
||||
"filename",
|
||||
"_storage_path",
|
||||
"cache_key",
|
||||
"storage",
|
||||
"state",
|
||||
"_to_dict",
|
||||
)
|
||||
|
||||
def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None:
|
||||
"""Initialize the DashboardEntry."""
|
||||
self.path = path
|
||||
self.filename = os.path.basename(path)
|
||||
self.filename: str = os.path.basename(path)
|
||||
self._storage_path = ext_storage_path(self.filename)
|
||||
self.cache_key = cache_key
|
||||
self.storage: StorageJSON | None = None
|
||||
self.state = EntryState.UNKNOWN
|
||||
self._to_dict: dict[str, Any] | None = None
|
||||
|
||||
def __repr__(self):
|
||||
"""Return the representation of this entry."""
|
||||
return (
|
||||
f"DashboardEntry({self.path} "
|
||||
f"DashboardEntry(path={self.path} "
|
||||
f"address={self.address} "
|
||||
f"web_port={self.web_port} "
|
||||
f"name={self.name} "
|
||||
f"no_mdns={self.no_mdns})"
|
||||
f"no_mdns={self.no_mdns} "
|
||||
f"state={self.state} "
|
||||
")"
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of this entry.
|
||||
|
||||
The dict includes the loaded configuration but not
|
||||
the current state of the entry.
|
||||
"""
|
||||
if self._to_dict is None:
|
||||
self._to_dict = {
|
||||
"name": self.name,
|
||||
"friendly_name": self.friendly_name,
|
||||
"configuration": self.filename,
|
||||
"loaded_integrations": self.loaded_integrations,
|
||||
"deployed_version": self.update_old,
|
||||
"current_version": self.update_new,
|
||||
"path": self.path,
|
||||
"comment": self.comment,
|
||||
"address": self.address,
|
||||
"web_port": self.web_port,
|
||||
"target_platform": self.target_platform,
|
||||
}
|
||||
return self._to_dict
|
||||
|
||||
def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None:
|
||||
"""Load this entry from disk."""
|
||||
self.storage = StorageJSON.load(self._storage_path)
|
||||
self._to_dict = None
|
||||
#
|
||||
# Currently StorageJSON.load() will return None if the file does not exist
|
||||
#
|
||||
|
|
19
esphome/dashboard/enum.py
Normal file
19
esphome/dashboard/enum.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
"""Enum backports from standard lib."""
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
class StrEnum(str, Enum):
|
||||
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
|
||||
|
||||
def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum:
|
||||
"""Create a new StrEnum instance."""
|
||||
if not isinstance(value, str):
|
||||
raise TypeError(f"{value!r} is not a string")
|
||||
return super().__new__(cls, value, *args, **kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return self.value."""
|
||||
return str(self.value)
|
|
@ -10,7 +10,9 @@ from esphome.zeroconf import (
|
|||
DashboardStatus,
|
||||
)
|
||||
|
||||
from ..const import SENTINEL
|
||||
from ..core import DASHBOARD
|
||||
from ..entries import bool_to_entry_state
|
||||
|
||||
|
||||
class MDNSStatus:
|
||||
|
@ -22,16 +24,16 @@ class MDNSStatus:
|
|||
self.aiozc: AsyncEsphomeZeroconf | None = None
|
||||
# This is the current mdns state for each host (True, False, None)
|
||||
self.host_mdns_state: dict[str, bool | None] = {}
|
||||
# This is the hostnames to filenames mapping
|
||||
self.host_name_to_filename: dict[str, str] = {}
|
||||
self.filename_to_host_name: dict[str, str] = {}
|
||||
# This is the hostnames to path mapping
|
||||
self.host_name_to_path: dict[str, str] = {}
|
||||
self.path_to_host_name: dict[str, str] = {}
|
||||
# This is a set of host names to track (i.e no_mdns = false)
|
||||
self.host_name_with_mdns_enabled: set[set] = set()
|
||||
self._loop = asyncio.get_running_loop()
|
||||
|
||||
def filename_to_host_name_thread_safe(self, filename: str) -> str | None:
|
||||
"""Resolve a filename to an address in a thread-safe manner."""
|
||||
return self.filename_to_host_name.get(filename)
|
||||
def get_path_to_host_name(self, path: str) -> str | None:
|
||||
"""Resolve a path to an address in a thread-safe manner."""
|
||||
return self.path_to_host_name.get(path)
|
||||
|
||||
async def async_resolve_host(self, host_name: str) -> str | None:
|
||||
"""Resolve a host name to an address in a thread-safe manner."""
|
||||
|
@ -42,14 +44,14 @@ class MDNSStatus:
|
|||
async def async_refresh_hosts(self):
|
||||
"""Refresh the hosts to track."""
|
||||
dashboard = DASHBOARD
|
||||
entries = dashboard.entries.async_all()
|
||||
current_entries = dashboard.entries.async_all()
|
||||
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
|
||||
host_mdns_state = self.host_mdns_state
|
||||
host_name_to_filename = self.host_name_to_filename
|
||||
filename_to_host_name = self.filename_to_host_name
|
||||
ping_result = dashboard.ping_result
|
||||
host_name_to_path = self.host_name_to_path
|
||||
path_to_host_name = self.path_to_host_name
|
||||
entries = dashboard.entries
|
||||
|
||||
for entry in entries:
|
||||
for entry in current_entries:
|
||||
name = entry.name
|
||||
# If no_mdns is set, remove it from the set
|
||||
if entry.no_mdns:
|
||||
|
@ -58,37 +60,37 @@ class MDNSStatus:
|
|||
|
||||
# We are tracking this host
|
||||
host_name_with_mdns_enabled.add(name)
|
||||
filename = entry.filename
|
||||
path = entry.path
|
||||
|
||||
# If we just adopted/imported this host, we likely
|
||||
# already have a state for it, so we should make sure
|
||||
# to set it so the dashboard shows it as online
|
||||
if name in host_mdns_state:
|
||||
ping_result[filename] = host_mdns_state[name]
|
||||
if (online := host_mdns_state.get(name, SENTINEL)) != SENTINEL:
|
||||
entries.async_set_state(entry, bool_to_entry_state(online))
|
||||
|
||||
# Make sure the mapping is up to date
|
||||
# so when we get an mdns update we can map it back
|
||||
# to the filename
|
||||
host_name_to_filename[name] = filename
|
||||
filename_to_host_name[filename] = name
|
||||
host_name_to_path[name] = path
|
||||
path_to_host_name[path] = name
|
||||
|
||||
async def async_run(self) -> None:
|
||||
dashboard = DASHBOARD
|
||||
|
||||
entries = dashboard.entries
|
||||
aiozc = AsyncEsphomeZeroconf()
|
||||
self.aiozc = aiozc
|
||||
host_mdns_state = self.host_mdns_state
|
||||
host_name_to_filename = self.host_name_to_filename
|
||||
host_name_to_path = self.host_name_to_path
|
||||
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
|
||||
ping_result = dashboard.ping_result
|
||||
|
||||
def on_update(dat: dict[str, bool | None]) -> None:
|
||||
"""Update the global PING_RESULT dict."""
|
||||
"""Update the entry state."""
|
||||
for name, result in dat.items():
|
||||
host_mdns_state[name] = result
|
||||
if name in host_name_with_mdns_enabled:
|
||||
filename = host_name_to_filename[name]
|
||||
ping_result[filename] = result
|
||||
if name not in host_name_with_mdns_enabled:
|
||||
continue
|
||||
if entry := entries.get(host_name_to_path[name]):
|
||||
entries.async_set_state(entry, bool_to_entry_state(result))
|
||||
|
||||
stat = DashboardStatus(on_update)
|
||||
imports = DashboardImportDiscovery()
|
||||
|
|
|
@ -8,6 +8,7 @@ import threading
|
|||
from esphome import mqtt
|
||||
|
||||
from ..core import DASHBOARD
|
||||
from ..entries import EntryState
|
||||
|
||||
|
||||
class MqttStatusThread(threading.Thread):
|
||||
|
@ -16,22 +17,23 @@ class MqttStatusThread(threading.Thread):
|
|||
def run(self) -> None:
|
||||
"""Run the status thread."""
|
||||
dashboard = DASHBOARD
|
||||
entries = dashboard.entries.all()
|
||||
entries = dashboard.entries
|
||||
current_entries = entries.all()
|
||||
|
||||
config = mqtt.config_from_env()
|
||||
topic = "esphome/discover/#"
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
nonlocal entries
|
||||
nonlocal current_entries
|
||||
|
||||
payload = msg.payload.decode(errors="backslashreplace")
|
||||
if len(payload) > 0:
|
||||
data = json.loads(payload)
|
||||
if "name" not in data:
|
||||
return
|
||||
for entry in entries:
|
||||
for entry in current_entries:
|
||||
if entry.name == data["name"]:
|
||||
dashboard.ping_result[entry.filename] = True
|
||||
entries.set_state(entry, EntryState.ONLINE)
|
||||
return
|
||||
|
||||
def on_connect(client, userdata, flags, return_code):
|
||||
|
@ -51,12 +53,11 @@ class MqttStatusThread(threading.Thread):
|
|||
client.loop_start()
|
||||
|
||||
while not dashboard.stop_event.wait(2):
|
||||
entries = dashboard.entries.all()
|
||||
|
||||
current_entries = entries.all()
|
||||
# will be set to true on on_message
|
||||
for entry in entries:
|
||||
for entry in current_entries:
|
||||
if entry.no_mdns:
|
||||
dashboard.ping_result[entry.filename] = False
|
||||
entries.set_state(entry, EntryState.OFFLINE)
|
||||
|
||||
client.publish("esphome/discover", None, retain=False)
|
||||
dashboard.mqtt_ping_request.wait()
|
||||
|
|
|
@ -5,7 +5,7 @@ import os
|
|||
from typing import cast
|
||||
|
||||
from ..core import DASHBOARD
|
||||
from ..entries import DashboardEntry
|
||||
from ..entries import DashboardEntry, bool_to_entry_state
|
||||
from ..util.itertools import chunked
|
||||
from ..util.subprocess import async_system_command_status
|
||||
|
||||
|
@ -26,14 +26,14 @@ class PingStatus:
|
|||
async def async_run(self) -> None:
|
||||
"""Run the ping status."""
|
||||
dashboard = DASHBOARD
|
||||
entries = dashboard.entries
|
||||
|
||||
while not dashboard.stop_event.is_set():
|
||||
# Only ping if the dashboard is open
|
||||
await dashboard.ping_request.wait()
|
||||
dashboard.ping_result.clear()
|
||||
entries = dashboard.entries.async_all()
|
||||
current_entries = dashboard.entries.async_all()
|
||||
to_ping: list[DashboardEntry] = [
|
||||
entry for entry in entries if entry.address is not None
|
||||
entry for entry in current_entries if entry.address is not None
|
||||
]
|
||||
for ping_group in chunked(to_ping, 16):
|
||||
ping_group = cast(list[DashboardEntry], ping_group)
|
||||
|
@ -46,4 +46,4 @@ class PingStatus:
|
|||
result = False
|
||||
elif isinstance(result, BaseException):
|
||||
raise result
|
||||
dashboard.ping_result[entry.filename] = result
|
||||
entries.async_set_state(entry, bool_to_entry_state(result))
|
||||
|
|
|
@ -37,6 +37,7 @@ from esphome.util import get_serial_ports, shlex_quote
|
|||
from esphome.yaml_util import FastestAvailableSafeLoader
|
||||
|
||||
from .core import DASHBOARD
|
||||
from .entries import EntryState, entry_state_to_bool
|
||||
from .util.subprocess import async_run_system_command
|
||||
from .util.text import friendly_name_slugify
|
||||
|
||||
|
@ -275,7 +276,7 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
|||
if (
|
||||
port == "OTA"
|
||||
and (mdns := dashboard.mdns_status)
|
||||
and (host_name := mdns.filename_to_host_name_thread_safe(configuration))
|
||||
and (host_name := mdns.get_path_to_host_name(config_file))
|
||||
and (address := await mdns.async_resolve_host(host_name))
|
||||
):
|
||||
port = address
|
||||
|
@ -315,7 +316,9 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
|
|||
return
|
||||
|
||||
# Remove the old ping result from the cache
|
||||
DASHBOARD.ping_result.pop(self.old_name, None)
|
||||
entries = DASHBOARD.entries
|
||||
if entry := entries.get(self.old_name):
|
||||
entries.async_set_state(entry, EntryState.UNKNOWN)
|
||||
|
||||
|
||||
class EsphomeUploadHandler(EsphomePortCommandWebSocket):
|
||||
|
@ -609,22 +612,7 @@ class ListDevicesHandler(BaseHandler):
|
|||
self.write(
|
||||
json.dumps(
|
||||
{
|
||||
"configured": [
|
||||
{
|
||||
"name": entry.name,
|
||||
"friendly_name": entry.friendly_name,
|
||||
"configuration": entry.filename,
|
||||
"loaded_integrations": entry.loaded_integrations,
|
||||
"deployed_version": entry.update_old,
|
||||
"current_version": entry.update_new,
|
||||
"path": entry.path,
|
||||
"comment": entry.comment,
|
||||
"address": entry.address,
|
||||
"web_port": entry.web_port,
|
||||
"target_platform": entry.target_platform,
|
||||
}
|
||||
for entry in entries
|
||||
],
|
||||
"configured": [entry.to_dict() for entry in entries],
|
||||
"importable": [
|
||||
{
|
||||
"name": res.device_name,
|
||||
|
@ -728,7 +716,15 @@ class PingRequestHandler(BaseHandler):
|
|||
if settings.status_use_mqtt:
|
||||
dashboard.mqtt_ping_request.set()
|
||||
self.set_header("content-type", "application/json")
|
||||
self.write(json.dumps(dashboard.ping_result))
|
||||
|
||||
self.write(
|
||||
json.dumps(
|
||||
{
|
||||
entry.filename: entry_state_to_bool(entry.state)
|
||||
for entry in dashboard.entries.async_all()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class InfoRequestHandler(BaseHandler):
|
||||
|
@ -785,9 +781,6 @@ class DeleteRequestHandler(BaseHandler):
|
|||
if build_folder is not None:
|
||||
shutil.rmtree(build_folder, os.path.join(trash_path, name))
|
||||
|
||||
# Remove the old ping result from the cache
|
||||
DASHBOARD.ping_result.pop(configuration, None)
|
||||
|
||||
|
||||
class UndoDeleteRequestHandler(BaseHandler):
|
||||
@authenticated
|
||||
|
|
Loading…
Reference in a new issue