diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py index 420f520..801fa3f 100644 --- a/custom_components/browser_mod/__init__.py +++ b/custom_components/browser_mod/__init__.py @@ -4,6 +4,7 @@ from .store import BrowserModStore from .mod_view import async_setup_view from .connection import async_setup_connection from .const import DOMAIN, DATA_DEVICES, DATA_ADDERS, DATA_STORE +from .service import async_setup_services _LOGGER = logging.getLogger(__name__) @@ -32,5 +33,6 @@ async def async_setup_entry(hass, config_entry): await async_setup_connection(hass) await async_setup_view(hass) + await async_setup_services(hass) return True diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index 2c561ec..f73094a 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -842,16 +842,14 @@ const ServicesMixin = (SuperClass) => { data: [title: ] [content: ] - [primary_action: ] - [secondary_action: ] + [right_button: ] + [right_button_action: ] + [left_button: ] + [left_button_action: ] [dismissable: ] + [dismiss_action: ] [timeout: ] - [callbacks: - [primary_action: ] - [secondary_action: ] - [timeout: ] - [dismissed: ] - ] + [timeout_action: ] Close popup service: browser_mod.close_popup @@ -861,18 +859,37 @@ const ServicesMixin = (SuperClass) => { data: message: */ - _service_action({ service, data }) { + constructor() { + super(); + const cmds = ["sequence", "delay", "popup", "close_popup"]; + for (const service of cmds) { + this.addEventListener(`command-${service}`, (ev) => { + this._service_action({ + service, + data: ev.detail, + }); + }); + } + } + async _service_action({ service, data }) { let _service = service; if (!_service.startsWith("browser_mod.") && _service.includes(".")) ; if (_service.startsWith("browser_mod.")) { _service = _service.substring(12); } switch (_service) { + case "sequence": + for (const a of data.sequence) + await this._service_action(a); + break; + case "delay": + await new Promise((resolve) => setTimeout(resolve, data.time)); + break; case "popup": const { title, content } = data, d = __rest(data, ["title", "content"]); - if (d.callbacks) { - for (const [k, v] of Object.entries(data.callbacks)) { - d.callbacks[k] = () => this._service_action(v); + for (const [k, v] of Object.entries(d)) { + if (k.endsWith("_action")) { + d[k] = () => this._service_action(v); } } this.showPopup(title, content, d); @@ -919,7 +936,7 @@ class BrowserModPopup extends s { }, 10); } } - async setupDialog(title, content, { primary_action = undefined, secondary_action = undefined, dismissable = true, timeout = undefined, callbacks = undefined, } = {}) { + async setupDialog(title, content, { right_button = undefined, right_button_action = undefined, left_button = undefined, left_button_action = undefined, dismissable = true, dismiss_action = undefined, timeout = undefined, timeout_action = undefined, } = {}) { this.title = title; if (content && typeof content === "object") { // Create a card from config in content @@ -932,40 +949,46 @@ class BrowserModPopup extends s { } else { // Basic HTML content + this.card = undefined; this.content = o(content); } - this.primary_action = primary_action; - this.secondary_action = secondary_action; - this.actions = primary_action === undefined ? undefined : ""; + this.right_button = right_button; + this.left_button = left_button; + this.actions = right_button === undefined ? undefined : ""; this.dismissable = dismissable; this.timeout = timeout; - this.callbacks = callbacks; + this._actions = { + right_button_action, + left_button_action, + dismiss_action, + timeout_action, + }; } _primary() { var _a, _b, _c; - if ((_a = this.callbacks) === null || _a === void 0 ? void 0 : _a.dismiss) - this.callbacks.dismiss = undefined; + if ((_a = this._actions) === null || _a === void 0 ? void 0 : _a.dismiss_action) + this._actions.dismiss_action = undefined; this.closeDialog(); - (_c = (_b = this.callbacks) === null || _b === void 0 ? void 0 : _b.primary_action) === null || _c === void 0 ? void 0 : _c.call(_b); + (_c = (_b = this._actions) === null || _b === void 0 ? void 0 : _b.right_button_action) === null || _c === void 0 ? void 0 : _c.call(_b); } _secondary() { var _a, _b, _c; - if ((_a = this.callbacks) === null || _a === void 0 ? void 0 : _a.dismiss) - this.callbacks.dismiss = undefined; + if ((_a = this._actions) === null || _a === void 0 ? void 0 : _a.dismiss_action) + this._actions.dismiss_action = undefined; this.closeDialog(); - (_c = (_b = this.callbacks) === null || _b === void 0 ? void 0 : _b.secondary_action) === null || _c === void 0 ? void 0 : _c.call(_b); + (_c = (_b = this._actions) === null || _b === void 0 ? void 0 : _b.left_button_action) === null || _c === void 0 ? void 0 : _c.call(_b); } _dismiss() { var _a, _b; this.closeDialog(); - (_b = (_a = this.callbacks) === null || _a === void 0 ? void 0 : _a.dismiss) === null || _b === void 0 ? void 0 : _b.call(_a); + (_b = (_a = this._actions) === null || _a === void 0 ? void 0 : _a.dismiss_action) === null || _b === void 0 ? void 0 : _b.call(_a); } _timeout() { var _a, _b, _c; - if ((_a = this.callbacks) === null || _a === void 0 ? void 0 : _a.dismiss) - this.callbacks.dismiss = undefined; + if ((_a = this._actions) === null || _a === void 0 ? void 0 : _a.dismiss_action) + this._actions.dismiss_action = undefined; this.closeDialog(); - (_c = (_b = this.callbacks) === null || _b === void 0 ? void 0 : _b.timeout) === null || _c === void 0 ? void 0 : _c.call(_b); + (_c = (_b = this._actions) === null || _b === void 0 ? void 0 : _b.timeout_action) === null || _c === void 0 ? void 0 : _c.call(_b); } render() { if (!this.open) @@ -999,20 +1022,20 @@ class BrowserModPopup extends s {
${this.content}
- ${this.primary_action !== undefined + ${this.right_button !== undefined ? $ ` ` : ""} - ${this.secondary_action !== undefined + ${this.left_button !== undefined ? $ ` ` @@ -1134,10 +1157,10 @@ __decorate([ ], BrowserModPopup.prototype, "card", void 0); __decorate([ e$2() -], BrowserModPopup.prototype, "primary_action", void 0); +], BrowserModPopup.prototype, "right_button", void 0); __decorate([ e$2() -], BrowserModPopup.prototype, "secondary_action", void 0); +], BrowserModPopup.prototype, "left_button", void 0); __decorate([ e$2() ], BrowserModPopup.prototype, "dismissable", void 0); @@ -1232,15 +1255,20 @@ var pjson = { /* TODO: + - Fix nomenclature + - Command -> Service + - Device -> Browser - Popups X Basic popups - Card-mod integration X Timeout X Fullscreen + - Motion/occupancy tracker - Information about interaction requirement - Information about fullykiosk - Commands - - Framework + - Rename browser_mod commands to browser_mod services + x Framework - ll-custom handling - Commands x popup @@ -1250,8 +1278,8 @@ var pjson = { - lovelace-reload - window-reload - screensaver - - sequence - - delay + x sequence + x delay - toast? - Redesign services to target devices - frontend editor for popup cards diff --git a/custom_components/browser_mod/service.py b/custom_components/browser_mod/service.py index 7c526f8..7b79dea 100644 --- a/custom_components/browser_mod/service.py +++ b/custom_components/browser_mod/service.py @@ -5,19 +5,73 @@ from homeassistant.helpers.entity_registry import ( async_entries_for_device, ) from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import device_registry, area_registry from .const import ( DOMAIN, DATA_DEVICES, DATA_ALIASES, USER_COMMANDS, - DATA_CONFIG, - CONFIG_DEVICES, ) _LOGGER = logging.getLogger(__name__) +async def async_setup_services(hass): + def call_service(service, targets, data): + + devices = hass.data[DOMAIN][DATA_DEVICES] + + if isinstance(targets, str): + targets = [targets] + + for target in targets: + if target not in devices: + continue + device = devices[target] + device.send(service, **data) + + def handle_service(call): + service = call.service + data = {**call.data} + device_ids = set(data.get("device_id", [])) + data.pop("device_id", None) + area_ids = set(data.get("area_id", [])) + data.pop("area_id", None) + targets = data.get("target", []) + if isinstance(targets, str): + targets = [targets] + targets = set(targets) + data.pop("target", None) + + dr = device_registry.async_get(hass) + + for device in device_ids: + dev = dr.async_get(device) + if not dev: + continue + browserID = list(dev.identifiers)[0][1] + if browserID is None: + continue + targets.add(browserID) + + for area in area_ids: + for dev in device_registry.async_entries_for_area(dr, area): + browserID = list(dev.identifiers)[0][1] + if browserID is None: + continue + targets.add(browserID) + + _LOGGER.error(service) + _LOGGER.error(targets) + _LOGGER.error(data) + + call_service(service, targets, data) + + hass.services.async_register(DOMAIN, "test", handle_service) + hass.services.async_register(DOMAIN, "popup", handle_service) + + async def setup_service(hass): def handle_command(call): command = call.data.get("command", None) @@ -49,55 +103,3 @@ async def setup_service(hass): hass.services.async_register(DOMAIN, "command", handle_command) for cmd in USER_COMMANDS: hass.services.async_register(DOMAIN, cmd.replace("-", "_"), command_wrapper) - - async def call_service(service_call): - await async_clean_devices(hass, service_call.data) - - hass.services.async_register(DOMAIN, "clean_devices", call_service) - - -async def async_clean_devices(hass, data): - config_entry = hass.config_entries.async_entries(DOMAIN)[0] - - entity_registry = await hass.helpers.entity_registry.async_get_registry() - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_entries = async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ) - - device_entries = [ - entry - for entry in device_registry.devices.values() - if config_entry.entry_id in entry.config_entries - ] - - user_config = hass.data[DOMAIN][DATA_CONFIG] - - devices_to_keep = [] - if CONFIG_DEVICES in user_config: - for d in device_entries: - for c in user_config[CONFIG_DEVICES]: - if (DOMAIN, c) in d.identifiers: - devices_to_keep.append(d.id) - - entities_to_remove = [] - for e in entity_entries: - entity = hass.states.get(e.entity_id) - if entity.state != STATE_UNAVAILABLE: - continue - if e.device_id in devices_to_keep: - continue - entities_to_remove.append(e) - - for e in entities_to_remove: - entity_registry.async_remove(e.entity_id) - - removed = [] - for d in device_entries: - if len(async_entries_for_device(entity_registry, d.id)) == 0: - removed.append(d.name) - device_registry.async_remove_device(d.id) - - devices = hass.data[DOMAIN][DATA_DEVICES] - for rec in devices: - devices[rec].send("toast", message=f"Removed devices: {removed}") diff --git a/custom_components/browser_mod/services.yaml b/custom_components/browser_mod/services.yaml index f32b31f..ac1419b 100644 --- a/custom_components/browser_mod/services.yaml +++ b/custom_components/browser_mod/services.yaml @@ -1,134 +1,93 @@ -command: - description: "Send a command to a browser." - fields: - command: - description: "Command to send" - example: "navigate" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -commands: - description: "Send several commands to a browser" - fields: - commands: - description: "List of commands to send" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -debug: - description: "On all browsers, show a popup, and a javascript alert with the current device ID." - fields: - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -set_theme: - description: "On all browsers, change the theme." - fields: - theme: - description: "Theme to change to" - example: '{theme: "clear_light"}' - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -navigate: - description: "Navigate to a path on a browser." - fields: - navigation_path: - description: "Path to navigate to" - example: "/lovelace/1" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -more_info: - description: "Open the more info dialog of an entity on a browser." - fields: - entity_id: - description: "Entity to show more info for" - example: "camera.front_door" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' - large: - description: "(optional) Set to true to make wider" - example: "true" -toast: - description: "Show a toast message in the bottom left on all browsers." - fields: - message: - description: "Message to show" - example: "Short message" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' - duration: - description: "(optional) Time in milliseconds to show message for. Set to 0 for persistent display." - example: "10000" +test: + description: "A debugging service" + target: + device: + integration: "browser_mod" + multiple: true + entity: + integration: "browser_mod_none" + area: + device: + integration: "browser_mod" + multiple: true + fields: {} + popup: - description: "Pop up a card on a browser." + description: "Display a popup" + target: + device: + integration: "browser_mod" + multiple: true + entity: + integration: "browser_mod_none" + area: + device: + integration: "browser_mod" + multiple: true fields: title: - description: "Name to show in popup bar" - example: "Popup example" - card: - description: "YAML config for card to show" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' - large: - description: "(optional) Set to true to make wider" - example: "true" - hide_header: - description: "(optional) Hide header title and close button" - example: "true" - auto_close: - description: "(optional) Close popup when mouse is moved or key is pressed. Also hides header" - example: "true" - time: - description: "(optional) When mouse isn't moved or keys aren't pressed for this amount of seconds, reopen. Only usable with auto_close. See blackout" - example: "20" + name: Title + description: "Popup title" + selector: + text: + content: + name: Content + required: true + description: "Popup content (Test or lovelace card configuration)" + selector: + object: + right_button: + name: Right button + description: Text of the right button + selector: + text: + right_button_action: + name: Right button action + description: Action to perform when the right button is pressed + selector: + object: + left_button: + name: Left button + description: Text of the left button + selector: + text: + left_button_action: + name: Left button action + description: Action to perform when left button is pressed + selector: + object: + dismissable: + name: User dismissable + description: Whether the popup can be closed by the user without action + default: true + selector: + boolean: + dismiss_action: + name: Dismiss action + description: Action to perform when popup is dismissed + selector: + object: + timeout: + name: Auto close timeout + description: Time before closing (ms) + selector: + number: + mode: box + timeout_action: + name: Timeout + description: Action to perform when popup is closed by timeout + selector: + object: + close_popup: - description: "Close all popups on all browsers." - fields: - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -blackout: - description: "Cover screen in black until the mouse is moved or a key is pressed." - fields: - time: - description: "(optional) The blackout will turn on automatically after the specified number of seconds. It works kind of like a screensaver and will keep turning on until blackout is called again with time: -1." - example: "20" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -no_blackout: - description: "Remove a blackout from a browser." - fields: - brightness: - description: "(optional) On a Fully Kiosk Browser Plus set the screen brightness from 0 - 255." - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -lovelace_reload: - description: "Refresh the lovelace configuration." - fields: - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -window_reload: - description: "Forces the browser to reload the page. Same as clicking your browser's refresh button. Note: This is not guaranteed to clear the frontend cache." - fields: - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -delay: - description: "Do nothing for a while" - fields: - seconds: - description: "Number of seconds to delay" - example: "5" - deviceID: - description: "(optional) List of receiving browsers" - example: '["99980b13-dabc9563", "office_computer"]' -call_service: - description: "" + description: "Close a popup" + target: + device: + integration: "browser_mod" + multiple: true + entity: + integration: "browser_mod_none" + area: + device: + integration: "browser_mod" + multiple: true diff --git a/js/plugin/main.ts b/js/plugin/main.ts index 3b230a5..22b8225 100644 --- a/js/plugin/main.ts +++ b/js/plugin/main.ts @@ -15,15 +15,20 @@ import pjson from "../../package.json"; /* TODO: + - Fix nomenclature + - Command -> Service + - Device -> Browser - Popups X Basic popups - Card-mod integration X Timeout X Fullscreen + - Motion/occupancy tracker - Information about interaction requirement - Information about fullykiosk - Commands - - Framework + - Rename browser_mod commands to browser_mod services + x Framework - ll-custom handling - Commands x popup @@ -33,8 +38,8 @@ import pjson from "../../package.json"; - lovelace-reload - window-reload - screensaver - - sequence - - delay + x sequence + x delay - toast? - Redesign services to target devices - frontend editor for popup cards diff --git a/js/plugin/popups.ts b/js/plugin/popups.ts index ca90a95..c9982ea 100644 --- a/js/plugin/popups.ts +++ b/js/plugin/popups.ts @@ -9,10 +9,10 @@ class BrowserModPopup extends LitElement { @property() title; @property({ reflect: true }) actions; @property({ reflect: true }) card; - @property() primary_action; - @property() secondary_action; + @property() right_button; + @property() left_button; @property() dismissable; - callbacks; + _actions; timeout; _timeoutStart; _timeoutTimer; @@ -39,11 +39,14 @@ class BrowserModPopup extends LitElement { title, content, { - primary_action = undefined, - secondary_action = undefined, + right_button = undefined, + right_button_action = undefined, + left_button = undefined, + left_button_action = undefined, dismissable = true, + dismiss_action = undefined, timeout = undefined, - callbacks = undefined, + timeout_action = undefined, } = {} ) { this.title = title; @@ -57,36 +60,42 @@ class BrowserModPopup extends LitElement { this.content = card; } else { // Basic HTML content + this.card = undefined; this.content = unsafeHTML(content); } - this.primary_action = primary_action; - this.secondary_action = secondary_action; - this.actions = primary_action === undefined ? undefined : ""; + this.right_button = right_button; + this.left_button = left_button; + this.actions = right_button === undefined ? undefined : ""; this.dismissable = dismissable; this.timeout = timeout; - this.callbacks = callbacks; + this._actions = { + right_button_action, + left_button_action, + dismiss_action, + timeout_action, + }; } _primary() { - if (this.callbacks?.dismiss) this.callbacks.dismiss = undefined; + if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined; this.closeDialog(); - this.callbacks?.primary_action?.(); + this._actions?.right_button_action?.(); } _secondary() { - if (this.callbacks?.dismiss) this.callbacks.dismiss = undefined; + if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined; this.closeDialog(); - this.callbacks?.secondary_action?.(); + this._actions?.left_button_action?.(); } _dismiss() { this.closeDialog(); - this.callbacks?.dismiss?.(); + this._actions?.dismiss_action?.(); } _timeout() { - if (this.callbacks?.dismiss) this.callbacks.dismiss = undefined; + if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined; this.closeDialog(); - this.callbacks?.timeout?.(); + this._actions?.timeout_action?.(); } render() { @@ -121,20 +130,20 @@ class BrowserModPopup extends LitElement {
${this.content}
- ${this.primary_action !== undefined + ${this.right_button !== undefined ? html` ` : ""} - ${this.secondary_action !== undefined + ${this.left_button !== undefined ? html` ` diff --git a/js/plugin/services.ts b/js/plugin/services.ts index 2098845..67ed4fa 100644 --- a/js/plugin/services.ts +++ b/js/plugin/services.ts @@ -23,16 +23,14 @@ export const ServicesMixin = (SuperClass) => { data: [title: ] [content: ] - [primary_action: ] - [secondary_action: ] + [right_button: ] + [right_button_action: ] + [left_button: ] + [left_button_action: ] [dismissable: ] + [dismiss_action: ] [timeout: ] - [callbacks: - [primary_action: ] - [secondary_action: ] - [timeout: ] - [dismissed: ] - ] + [timeout_action: ] Close popup service: browser_mod.close_popup @@ -43,7 +41,20 @@ export const ServicesMixin = (SuperClass) => { message: */ - _service_action({ service, data }) { + constructor() { + super(); + const cmds = ["sequence", "delay", "popup", "close_popup"]; + for (const service of cmds) { + this.addEventListener(`command-${service}`, (ev) => { + this._service_action({ + service, + data: ev.detail, + }); + }); + } + } + + async _service_action({ service, data }) { let _service: String = service; if (!_service.startsWith("browser_mod.") && _service.includes(".")) { // CALL HOME ASSISTANT SERVICE @@ -54,11 +65,18 @@ export const ServicesMixin = (SuperClass) => { } switch (_service) { + case "sequence": + for (const a of data.sequence) await this._service_action(a); + break; + case "delay": + await new Promise((resolve) => setTimeout(resolve, data.time)); + break; + case "popup": const { title, content, ...d } = data; - if (d.callbacks) { - for (const [k, v] of Object.entries(data.callbacks)) { - d.callbacks[k] = () => this._service_action(v as any); + for (const [k, v] of Object.entries(d)) { + if (k.endsWith("_action")) { + d[k] = () => this._service_action(v as any); } } this.showPopup(title, content, d);