Block repeated on while auto-off is active
This commit is contained in:
@@ -350,7 +350,7 @@ class Action:
|
|||||||
if not key:
|
if not key:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
prefix = self.cfg.get("camera_name") or self.cfg.get("frigate_name") or ""
|
prefix = self.cfg.get("camera_display_name") or self.cfg.get("camera_name") or self.cfg.get("frigate_name") or ""
|
||||||
return text_for(normalized, key, target=prefix).strip()
|
return text_for(normalized, key, target=prefix).strip()
|
||||||
|
|
||||||
def command_list(self):
|
def command_list(self):
|
||||||
@@ -807,12 +807,14 @@ class CameraControlBridge:
|
|||||||
urls = self.camera_candidate_urls(camera_name, camera_cfg, streams)
|
urls = self.camera_candidate_urls(camera_name, camera_cfg, streams)
|
||||||
endpoints = self.camera_endpoints_from_urls(urls)
|
endpoints = self.camera_endpoints_from_urls(urls)
|
||||||
kinds = self.kinds_from_urls(urls)
|
kinds = self.kinds_from_urls(urls)
|
||||||
|
camera_display_name = self.camera_display_name(camera_name, camera_cfg)
|
||||||
for host in endpoints:
|
for host in endpoints:
|
||||||
inventory.append(
|
inventory.append(
|
||||||
{
|
{
|
||||||
"frigate": name,
|
"frigate": name,
|
||||||
"frigate_url": base_url,
|
"frigate_url": base_url,
|
||||||
"camera": camera_name,
|
"camera": camera_name,
|
||||||
|
"camera_display_name": camera_display_name,
|
||||||
"host": host,
|
"host": host,
|
||||||
"kinds": kinds,
|
"kinds": kinds,
|
||||||
}
|
}
|
||||||
@@ -820,6 +822,15 @@ class CameraControlBridge:
|
|||||||
LOG.info("discovered %d camera host mapping(s) from Frigate", len(inventory))
|
LOG.info("discovered %d camera host mapping(s) from Frigate", len(inventory))
|
||||||
return inventory
|
return inventory
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def camera_display_name(camera_name, camera_cfg):
|
||||||
|
# Frigate camera keys remain the API identifier. friendly_name is only
|
||||||
|
# used for human-facing labels when present.
|
||||||
|
friendly_name = camera_cfg.get("friendly_name")
|
||||||
|
if friendly_name:
|
||||||
|
return str(friendly_name)
|
||||||
|
return camera_name
|
||||||
|
|
||||||
def load_frigate_config_with_retry(self, frigate, base_url):
|
def load_frigate_config_with_retry(self, frigate, base_url):
|
||||||
discovery = self.config.get("discovery", {})
|
discovery = self.config.get("discovery", {})
|
||||||
wait = bool(discovery.get("wait_for_frigate", True))
|
wait = bool(discovery.get("wait_for_frigate", True))
|
||||||
@@ -1081,6 +1092,7 @@ class CameraControlBridge:
|
|||||||
"name": text_for(lang, "action.manual_white", target=item["camera"]),
|
"name": text_for(lang, "action.manual_white", target=item["camera"]),
|
||||||
"name_key": "action.manual_white",
|
"name_key": "action.manual_white",
|
||||||
"camera_name": item["camera"],
|
"camera_name": item["camera"],
|
||||||
|
"camera_display_name": item.get("camera_display_name", item["camera"]),
|
||||||
"platform": "switch",
|
"platform": "switch",
|
||||||
"kind": "hikvision_colorvu",
|
"kind": "hikvision_colorvu",
|
||||||
"host": host,
|
"host": host,
|
||||||
@@ -1089,7 +1101,7 @@ class CameraControlBridge:
|
|||||||
"snapshot": self.snapshot_config(item),
|
"snapshot": self.snapshot_config(item),
|
||||||
"device": {
|
"device": {
|
||||||
"identifiers": [f"camera_{host.replace('.', '_')}"],
|
"identifiers": [f"camera_{host.replace('.', '_')}"],
|
||||||
"name": item["camera"],
|
"name": item.get("camera_display_name", item["camera"]),
|
||||||
"manufacturer": "Hikvision",
|
"manufacturer": "Hikvision",
|
||||||
"model": model,
|
"model": model,
|
||||||
"configuration_url": item["frigate_url"],
|
"configuration_url": item["frigate_url"],
|
||||||
@@ -1120,6 +1132,7 @@ class CameraControlBridge:
|
|||||||
"name": text_for(lang, "action.red_blue", target=item["camera"]),
|
"name": text_for(lang, "action.red_blue", target=item["camera"]),
|
||||||
"name_key": "action.red_blue",
|
"name_key": "action.red_blue",
|
||||||
"camera_name": item["camera"],
|
"camera_name": item["camera"],
|
||||||
|
"camera_display_name": item.get("camera_display_name", item["camera"]),
|
||||||
"platform": "switch",
|
"platform": "switch",
|
||||||
"kind": "dahua_coaxial_io",
|
"kind": "dahua_coaxial_io",
|
||||||
"host": host,
|
"host": host,
|
||||||
@@ -1138,6 +1151,7 @@ class CameraControlBridge:
|
|||||||
"name": text_for(lang, "action.siren", target=item["camera"]),
|
"name": text_for(lang, "action.siren", target=item["camera"]),
|
||||||
"name_key": "action.siren",
|
"name_key": "action.siren",
|
||||||
"camera_name": item["camera"],
|
"camera_name": item["camera"],
|
||||||
|
"camera_display_name": item.get("camera_display_name", item["camera"]),
|
||||||
"platform": "switch",
|
"platform": "switch",
|
||||||
"kind": "dahua_coaxial_io",
|
"kind": "dahua_coaxial_io",
|
||||||
"host": host,
|
"host": host,
|
||||||
@@ -1182,7 +1196,7 @@ class CameraControlBridge:
|
|||||||
def discovered_device(item, manufacturer, model):
|
def discovered_device(item, manufacturer, model):
|
||||||
return {
|
return {
|
||||||
"identifiers": [f"camera_{item['host'].replace('.', '_')}"],
|
"identifiers": [f"camera_{item['host'].replace('.', '_')}"],
|
||||||
"name": item["camera"],
|
"name": item.get("camera_display_name", item["camera"]),
|
||||||
"manufacturer": manufacturer,
|
"manufacturer": manufacturer,
|
||||||
"model": model,
|
"model": model,
|
||||||
"configuration_url": item["frigate_url"],
|
"configuration_url": item["frigate_url"],
|
||||||
@@ -1192,6 +1206,7 @@ class CameraControlBridge:
|
|||||||
config = {
|
config = {
|
||||||
"frigate_url": item["frigate_url"],
|
"frigate_url": item["frigate_url"],
|
||||||
"camera": item["camera"],
|
"camera": item["camera"],
|
||||||
|
"camera_display_name": item.get("camera_display_name", item["camera"]),
|
||||||
}
|
}
|
||||||
defaults = self.config.get("defaults", {})
|
defaults = self.config.get("defaults", {})
|
||||||
if defaults.get("frigate_public_url"):
|
if defaults.get("frigate_public_url"):
|
||||||
@@ -1413,6 +1428,8 @@ class CameraControlBridge:
|
|||||||
action = self.action_by_id(action_id)
|
action = self.action_by_id(action_id)
|
||||||
if action is None:
|
if action is None:
|
||||||
raise RuntimeError(f"unknown action {action_id}")
|
raise RuntimeError(f"unknown action {action_id}")
|
||||||
|
if command == "on" and self.auto_off_running(action):
|
||||||
|
raise RuntimeError("action is already active until automatic turn-off")
|
||||||
result = action.run(command, bridge=self)
|
result = action.run(command, bridge=self)
|
||||||
if action.platform == "switch":
|
if action.platform == "switch":
|
||||||
if command == "on":
|
if command == "on":
|
||||||
@@ -1428,6 +1445,10 @@ class CameraControlBridge:
|
|||||||
LOG.warning("action %s %s worked, but MQTT publish failed: %s", action_id, command, exc)
|
LOG.warning("action %s %s worked, but MQTT publish failed: %s", action_id, command, exc)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def auto_off_running(action):
|
||||||
|
return bool(getattr(action, "auto_off_at", None))
|
||||||
|
|
||||||
def schedule_auto_off(self, action):
|
def schedule_auto_off(self, action):
|
||||||
seconds = int(action.auto_off_seconds or 0)
|
seconds = int(action.auto_off_seconds or 0)
|
||||||
if seconds <= 0:
|
if seconds <= 0:
|
||||||
@@ -1723,7 +1744,9 @@ class CameraControlBridge:
|
|||||||
if parsed.path.startswith("/snapshot/"):
|
if parsed.path.startswith("/snapshot/"):
|
||||||
action_id = urllib.parse.unquote(parsed.path.rsplit("/", 1)[-1]).removesuffix(".jpg")
|
action_id = urllib.parse.unquote(parsed.path.rsplit("/", 1)[-1]).removesuffix(".jpg")
|
||||||
try:
|
try:
|
||||||
live = bool(bridge.config.get("defaults", {}).get("snapshot_live_on_gui", True))
|
values = urllib.parse.parse_qs(parsed.query)
|
||||||
|
refresh = values.get("refresh", ["0"])[0].lower() in ("1", "true", "yes", "on")
|
||||||
|
live = refresh or bool(bridge.config.get("defaults", {}).get("snapshot_live_on_gui", True))
|
||||||
snapshot = bridge.snapshot_for_action(action_id, live=live)
|
snapshot = bridge.snapshot_for_action(action_id, live=live)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOG.info("snapshot request failed for %s: %s", action_id, exc)
|
LOG.info("snapshot request failed for %s: %s", action_id, exc)
|
||||||
|
|||||||
@@ -213,6 +213,45 @@ select {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.snapshot-refresh {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
display: grid;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(15, 23, 42, 0.72);
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-refresh:hover {
|
||||||
|
background: rgba(15, 23, 42, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-refresh:focus-visible {
|
||||||
|
outline: 3px solid rgba(29, 78, 216, 0.35);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-refresh.is-refreshing svg {
|
||||||
|
animation: refresh-spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-refresh svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.media-empty {
|
.media-empty {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -514,6 +553,13 @@ h3 {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmd:disabled {
|
||||||
|
border-color: var(--line);
|
||||||
|
background: #eef2f6;
|
||||||
|
color: #8290a2;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.cmd-on,
|
.cmd-on,
|
||||||
.cmd-press {
|
.cmd-press {
|
||||||
border-color: var(--blue);
|
border-color: var(--blue);
|
||||||
@@ -521,12 +567,24 @@ h3 {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmd-on:disabled {
|
||||||
|
border-color: var(--line);
|
||||||
|
background: #eef2f6;
|
||||||
|
color: #8290a2;
|
||||||
|
}
|
||||||
|
|
||||||
.cmd-on:hover,
|
.cmd-on:hover,
|
||||||
.cmd-press:hover {
|
.cmd-press:hover {
|
||||||
border-color: var(--blue-dark);
|
border-color: var(--blue-dark);
|
||||||
background: var(--blue-dark);
|
background: var(--blue-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmd-on:disabled:hover {
|
||||||
|
border-color: var(--line);
|
||||||
|
background: #eef2f6;
|
||||||
|
color: #8290a2;
|
||||||
|
}
|
||||||
|
|
||||||
.cmd-off {
|
.cmd-off {
|
||||||
border-color: #9dd8bd;
|
border-color: #9dd8bd;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|||||||
@@ -100,6 +100,39 @@ async function handleRefreshSubmit(form, event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshSnapshot(button) {
|
||||||
|
if (button.classList.contains("is-refreshing")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const media = button.closest(".media");
|
||||||
|
const image = media?.querySelector("img.snapshot");
|
||||||
|
if (!image) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = new URL(image.currentSrc || image.src, window.location.href);
|
||||||
|
url.searchParams.set("refresh", "1");
|
||||||
|
url.searchParams.set("v", String(Date.now()));
|
||||||
|
const cleanup = () => {
|
||||||
|
button.disabled = false;
|
||||||
|
button.classList.remove("is-refreshing");
|
||||||
|
button.removeAttribute("aria-busy");
|
||||||
|
};
|
||||||
|
button.disabled = true;
|
||||||
|
button.classList.add("is-refreshing");
|
||||||
|
button.setAttribute("aria-busy", "true");
|
||||||
|
image.addEventListener("load", cleanup, { once: true });
|
||||||
|
image.addEventListener("error", cleanup, { once: true });
|
||||||
|
image.src = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
const button = event.target.closest(".snapshot-refresh");
|
||||||
|
if (!(button instanceof HTMLButtonElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshSnapshot(button);
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("submit", (event) => {
|
document.addEventListener("submit", (event) => {
|
||||||
const form = event.target;
|
const form = event.target;
|
||||||
if (!(form instanceof HTMLFormElement)) {
|
if (!(form instanceof HTMLFormElement)) {
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ def camera_group_key(action):
|
|||||||
|
|
||||||
def camera_group_name(action, lang):
|
def camera_group_name(action, lang):
|
||||||
return (
|
return (
|
||||||
action.cfg.get("camera_name")
|
action.cfg.get("camera_display_name")
|
||||||
|
or (action.cfg.get("snapshot") or {}).get("camera_display_name")
|
||||||
|
or action.cfg.get("camera_name")
|
||||||
or (action.cfg.get("snapshot") or {}).get("camera")
|
or (action.cfg.get("snapshot") or {}).get("camera")
|
||||||
or action.cfg.get("device", {}).get("name")
|
or action.cfg.get("device", {}).get("name")
|
||||||
or action.display_name(lang)
|
or action.display_name(lang)
|
||||||
@@ -140,6 +142,7 @@ def snapshot_markup_for_group(group, lang, token_query):
|
|||||||
actions = group["actions"]
|
actions = group["actions"]
|
||||||
action = next((item for item in actions if item.cfg.get("snapshot")), actions[0])
|
action = next((item for item in actions if item.cfg.get("snapshot")), actions[0])
|
||||||
icons = group_visual_markup(actions, lang)
|
icons = group_visual_markup(actions, lang)
|
||||||
|
refresh_label = text_for(lang, "button.refresh_snapshot")
|
||||||
snapshot_markup = (
|
snapshot_markup = (
|
||||||
'<div class="media media-empty">'
|
'<div class="media media-empty">'
|
||||||
f"{icons}"
|
f"{icons}"
|
||||||
@@ -157,6 +160,15 @@ def snapshot_markup_for_group(group, lang, token_query):
|
|||||||
f'<img class="snapshot" src="{html.escape(snapshot_url)}" '
|
f'<img class="snapshot" src="{html.escape(snapshot_url)}" '
|
||||||
f'alt="{html.escape(group["name"])}" loading="lazy" '
|
f'alt="{html.escape(group["name"])}" loading="lazy" '
|
||||||
'onerror="this.parentElement.classList.add(\'media-empty\');this.remove()">'
|
'onerror="this.parentElement.classList.add(\'media-empty\');this.remove()">'
|
||||||
|
f'<button class="snapshot-refresh" type="button" title="{html.escape(refresh_label)}" '
|
||||||
|
f'aria-label="{html.escape(refresh_label)}">'
|
||||||
|
'<svg viewBox="0 0 24 24" aria-hidden="true">'
|
||||||
|
'<path d="M20 6v5h-5"></path>'
|
||||||
|
'<path d="M4 18v-5h5"></path>'
|
||||||
|
'<path d="M18.6 9A7 7 0 0 0 6.3 6.4L4 8.6"></path>'
|
||||||
|
'<path d="M5.4 15A7 7 0 0 0 17.7 17.6L20 15.4"></path>'
|
||||||
|
"</svg>"
|
||||||
|
"</button>"
|
||||||
f"{icons}"
|
f"{icons}"
|
||||||
"</div>"
|
"</div>"
|
||||||
)
|
)
|
||||||
@@ -296,9 +308,17 @@ def render_action_buttons(action, lang, token_query):
|
|||||||
' data-requires-ack="true"'
|
' data-requires-ack="true"'
|
||||||
f' data-ack-message="{html.escape(text_for(lang, "ack.siren_on"))}"'
|
f' data-ack-message="{html.escape(text_for(lang, "ack.siren_on"))}"'
|
||||||
)
|
)
|
||||||
|
disabled_attrs = ""
|
||||||
|
if command == "on" and getattr(action, "auto_off_at", None):
|
||||||
|
disabled_label = text_for(lang, "button.on_blocked_timer")
|
||||||
|
disabled_attrs = (
|
||||||
|
" disabled"
|
||||||
|
f' title="{html.escape(disabled_label)}"'
|
||||||
|
f' aria-label="{html.escape(disabled_label)}"'
|
||||||
|
)
|
||||||
buttons.append(
|
buttons.append(
|
||||||
f'<form class="command-form" method="post" action="{html.escape(action_url)}"{ack_attrs}>'
|
f'<form class="command-form" method="post" action="{html.escape(action_url)}"{ack_attrs}>'
|
||||||
f'<button class="cmd cmd-{html.escape(command)}" type="submit">'
|
f'<button class="cmd cmd-{html.escape(command)}" type="submit"{disabled_attrs}>'
|
||||||
f"<span>{html.escape(label)}</span>"
|
f"<span>{html.escape(label)}</span>"
|
||||||
"</button></form>"
|
"</button></form>"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ msgstr "PRESS"
|
|||||||
msgid "button.refresh_cameras"
|
msgid "button.refresh_cameras"
|
||||||
msgstr "Refresh cameras"
|
msgstr "Refresh cameras"
|
||||||
|
|
||||||
|
msgid "button.refresh_snapshot"
|
||||||
|
msgstr "Refresh snapshot"
|
||||||
|
|
||||||
|
msgid "button.on_blocked_timer"
|
||||||
|
msgstr "Already active until automatic turn-off"
|
||||||
|
|
||||||
msgid "link.open_stream"
|
msgid "link.open_stream"
|
||||||
msgstr "Open Frigate stream"
|
msgstr "Open Frigate stream"
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ msgstr "URUCHOM"
|
|||||||
msgid "button.refresh_cameras"
|
msgid "button.refresh_cameras"
|
||||||
msgstr "Odswiez kamery"
|
msgstr "Odswiez kamery"
|
||||||
|
|
||||||
|
msgid "button.refresh_snapshot"
|
||||||
|
msgstr "Odswiez obraz"
|
||||||
|
|
||||||
|
msgid "button.on_blocked_timer"
|
||||||
|
msgstr "Juz wlaczone do czasu automatycznego wylaczenia"
|
||||||
|
|
||||||
msgid "link.open_stream"
|
msgid "link.open_stream"
|
||||||
msgstr "Otworz stream we Frigate"
|
msgstr "Otworz stream we Frigate"
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class DummyAction:
|
|||||||
def __init__(self, action_id, camera=None, kind="hikvision_colorvu", name_key=None):
|
def __init__(self, action_id, camera=None, kind="hikvision_colorvu", name_key=None):
|
||||||
self.id = action_id
|
self.id = action_id
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
|
self.platform = "switch"
|
||||||
|
self.auto_off_at = None
|
||||||
|
self.run_called = False
|
||||||
self.cfg = {}
|
self.cfg = {}
|
||||||
if name_key:
|
if name_key:
|
||||||
self.cfg["name_key"] = name_key
|
self.cfg["name_key"] = name_key
|
||||||
@@ -16,6 +19,10 @@ class DummyAction:
|
|||||||
"camera": camera,
|
"camera": camera,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def run(self, command, bridge=None):
|
||||||
|
self.run_called = True
|
||||||
|
return {"ok": True, "command": command}
|
||||||
|
|
||||||
|
|
||||||
class RefreshSummaryTest(unittest.TestCase):
|
class RefreshSummaryTest(unittest.TestCase):
|
||||||
def test_camera_keys_ignore_global_actions(self):
|
def test_camera_keys_ignore_global_actions(self):
|
||||||
@@ -28,6 +35,18 @@ class RefreshSummaryTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(keys, {("http://frigate.example.test:5000", "one")})
|
self.assertEqual(keys, {("http://frigate.example.test:5000", "one")})
|
||||||
|
|
||||||
|
def test_camera_display_name_uses_friendly_name(self):
|
||||||
|
self.assertEqual(
|
||||||
|
CameraControlBridge.camera_display_name("camera_key", {"friendly_name": "Camera Friendly"}),
|
||||||
|
"Camera Friendly",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_camera_display_name_falls_back_to_config_name(self):
|
||||||
|
self.assertEqual(
|
||||||
|
CameraControlBridge.camera_display_name("camera_key", {}),
|
||||||
|
"camera_key",
|
||||||
|
)
|
||||||
|
|
||||||
def test_refresh_message_reports_camera_changes_first(self):
|
def test_refresh_message_reports_camera_changes_first(self):
|
||||||
message = CameraControlBridge.refresh_message(
|
message = CameraControlBridge.refresh_message(
|
||||||
"en",
|
"en",
|
||||||
@@ -64,6 +83,17 @@ class RefreshSummaryTest(unittest.TestCase):
|
|||||||
self.assertFalse(CameraControlBridge.should_refresh_snapshot_after_command(siren, "on"))
|
self.assertFalse(CameraControlBridge.should_refresh_snapshot_after_command(siren, "on"))
|
||||||
self.assertFalse(CameraControlBridge.should_refresh_snapshot_after_command(white, "press"))
|
self.assertFalse(CameraControlBridge.should_refresh_snapshot_after_command(white, "press"))
|
||||||
|
|
||||||
|
def test_execute_action_rejects_second_on_while_timer_runs(self):
|
||||||
|
action = DummyAction("white", "one", name_key="action.manual_white")
|
||||||
|
action.auto_off_at = 1234567890
|
||||||
|
bridge = object.__new__(CameraControlBridge)
|
||||||
|
bridge.action_by_id = lambda action_id: action
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "already active"):
|
||||||
|
CameraControlBridge.execute_action(bridge, "white", "on", publish=False)
|
||||||
|
|
||||||
|
self.assertFalse(action.run_called)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class UiRenderTest(unittest.TestCase):
|
|||||||
self.assertIn('data-requires-ack="true"', html)
|
self.assertIn('data-requires-ack="true"', html)
|
||||||
self.assertIn("Example camera", html)
|
self.assertIn("Example camera", html)
|
||||||
self.assertIn("v=42", html)
|
self.assertIn("v=42", html)
|
||||||
|
self.assertIn('class="snapshot-refresh"', html)
|
||||||
|
self.assertIn('title="Refresh snapshot"', html)
|
||||||
|
self.assertIn('disabled title="Already active until automatic turn-off"', html)
|
||||||
|
|
||||||
def test_controls_from_same_camera_share_one_card(self):
|
def test_controls_from_same_camera_share_one_card(self):
|
||||||
actions = [
|
actions = [
|
||||||
@@ -92,6 +95,25 @@ class UiRenderTest(unittest.TestCase):
|
|||||||
"https://frigate.example.test/#example",
|
"https://frigate.example.test/#example",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_on_button_is_available_when_no_auto_off_timer_runs(self):
|
||||||
|
action = DummyAction()
|
||||||
|
action.auto_off_at = None
|
||||||
|
|
||||||
|
html = render_page([action], "en")
|
||||||
|
|
||||||
|
self.assertIn('class="cmd cmd-on"', html)
|
||||||
|
self.assertNotIn('Already active until automatic turn-off', html)
|
||||||
|
|
||||||
|
def test_camera_card_uses_friendly_display_name(self):
|
||||||
|
action = DummyAction()
|
||||||
|
action.cfg["camera_name"] = "camera_key"
|
||||||
|
action.cfg["camera_display_name"] = "Camera Friendly"
|
||||||
|
|
||||||
|
html = render_page([action], "en")
|
||||||
|
|
||||||
|
self.assertIn("<h2>Camera Friendly</h2>", html)
|
||||||
|
self.assertNotIn("<h2>camera_key</h2>", html)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user