Compare commits

..

11 Commits

Author SHA1 Message Date
1a3a7b2e95 Tag 2.0 🎉 2022-09-02 21:18:38 +00:00
98aaacba81 Link to device from browser mod panel 2022-09-02 21:12:39 +00:00
e2f1dfdaf2 Recompile stuff 2022-09-02 19:35:17 +00:00
46b1f05e80 Await async stuff 2022-09-02 19:34:32 +00:00
ee654d32c4 Fix auto interact in fully kiosk 2022-09-02 19:34:22 +00:00
93aaa96406 Allow more-info from popups 2022-09-01 20:15:27 +00:00
87c8f91701 Tweak popup behavior on mobile 2022-09-01 19:59:23 +00:00
79cb58175b Update readme 2022-09-01 19:23:30 +00:00
fffb017287 Easier to use frontend options. Set sidebar title. 2022-09-01 15:38:23 +00:00
39f727206f Improved thread safety 2022-08-16 20:42:35 +00:00
ae9ffb65c1 Remove temporary blur effect 2022-08-04 20:36:17 +00:00
30 changed files with 1094 additions and 605 deletions

View File

@@ -1,12 +1,31 @@
# browser_mod 2.0 # browser_mod 2.0
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs)
What if that tablet you have on your wall could open up a live feed from your front door camera when someone rings the bell?
And what if you could use it as an extra security camera?
Or what if you could use it to play music and videos from your Home Assistant media library?
What if you could permanently hide that sidebar from your kids and lock them into a single dashboard?
What if you could change the icon of the Home Assistant tab so it doesn't look the same as the forum?
What if you could change the more-info dialog for some entity to a dashboard card of your own design?
What if you could tap a button and have Home Assistant ask you which rooms you want the roomba to vacuum?
\
 
# Installation instructions # Installation instructions
- **First make sure you have completely removed any installation of Browser Mod 1** - **First make sure you have completely removed any installation of Browser Mod 1**
- Either - Either
- ~~Find and install Browser Mod under `integrations`in [HACS](https://hacs.xyz)~~ - Find and install Browser Mod under `integrations`in [HACS](https://hacs.xyz)
- OR copy the contents of `custom_components/browser_mod/` to `<your config dir>/custom_components/browser_mod/`. - OR copy the contents of `custom_components/browser_mod/` to `<your config dir>/custom_components/browser_mod/`.
- Restart Home Assistant - Restart Home Assistant
@@ -15,49 +34,31 @@
- Restart Home Assistant - Restart Home Assistant
> Note: If you are upgrading from Browser Mod 1, it is likely that you will get some errors in your log during a transition period. They will say something along the lines of `Error handling message: extra keys not allowed @ data['deviceID']`.
>
> They appear when a browser which has an old version of Browser Mod cached tries to connect and should disappear once you have cleared all your caches properly.
\
&nbsp;
# Browser Mod Configuration Panel # Browser Mod Configuration Panel
When you're logged in as an administrator you should see a new panel called _Browser Mod_ in the sidebar. This is where you controll any Browser Mod settings. When you're logged in as an administrator you should see a new panel called _Browser Mod_ in the sidebar. This is where you controll any Browser Mod settings.
## See [Configuration Panel](documentation/configuration-panel.md) for more info ### See [Configuration Panel](documentation/configuration-panel.md) for more info
\
&nbsp;
# Browser Mod Services # Browser Mod Services
Browser Mod has a number of services you can call to cause things to happen in the target Browser. Browser Mod has a number of services you can call to cause things to happen in the target Browser, such as opening a popup or navigating to a certain dashboard.
## See [Services](documentation/services.md) for more info ### See [Services](documentation/services.md) for more info
\
&nbsp;
### Calling services
Services can be called from the backend using the normal service call procedures. Registered Browsers can be selected as targets through their device: ## Popup card
![GUI service call](https://user-images.githubusercontent.com/1299821/180668350-1cbe751d-615d-4102-b939-e49e9cd2ca74.png)
In yaml, the BrowserID can be used for targeting a specific browser:
```yaml
service: browser_mod.more_info
data:
entity: light.bed_light
browser_id:
- 79be65e8-f06c78f
```
If no target or `browser_id` is specified, the service will target all registerd Browsers.
To call a service from a dashboard use the call-service [action](https://www.home-assistant.io/dashboards/actions/) or the special action `fire-dom-event`:
```yaml
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.more_info
data:
entity: light.bed_light
```
Services called via `fire-dom-event` or called as a part of a different service call will (by default) _only_ target the current Browser (even if it's not registered).
# Popup card
A popup card can be used to replace the more-info dialog of an entity with something of your choosing. A popup card can be used to replace the more-info dialog of an entity with something of your choosing.
@@ -78,8 +79,9 @@ card:
[any parameter from the browser_mod.popup service call except "content"] [any parameter from the browser_mod.popup service call except "content"]
``` ```
> *Note:* It's advisable to use a `fire-dom-event` tap action instead as far as possible. Popup card is for the few cases where that's not possible. See [`services`](documentation/services.md) for more info.
# Browser Player ## Browser Player
Browser player is a card that allows you to controll the volume and playback on the current Browsers media player. Browser player is a card that allows you to controll the volume and playback on the current Browsers media player.
@@ -89,6 +91,18 @@ Add it to a dashboard via the GUI or through yaml:
type: custom:browser-player type: custom:browser-player
``` ```
# FAQ
### **Why doesn't ANYTHING that used to work with Browser Mod 1.0 work with Browser Mod 2.0?**
Browser Mod 2.0 has been rewritten ENTIRELY from the ground up. This allows it to be more stable and less resource intensive. At the same time I took the opportunity to rename a lot of things in ways that are more consistent with Home Assistant nomenclature.
In short, things are hopefully much easier now for new users of Browser Mod at the unfortunate cost of a one-time inconvenience for veteran expert users such as yourself.
### **Why does my Browser ID keep changing?**
There's just no way around this. I've used every trick in the book and invented a handful of new ones in order to save the Browser ID as far as possible. It should be much better in Browser Mod 2.0 than earlier, but it's still not perfect. At least it's easy to change it back now...
--- ---
<a href="https://www.buymeacoffee.com/uqD6KHCdJ" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/white_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a> <a href="https://www.buymeacoffee.com/uqD6KHCdJ" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/white_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>

View File

@@ -3,6 +3,7 @@ import logging
from homeassistant.components.websocket_api import event_message from homeassistant.components.websocket_api import event_message
from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers import device_registry, entity_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.core import callback
from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS
from .sensor import BrowserSensor from .sensor import BrowserSensor
@@ -130,11 +131,14 @@ class BrowserModBrowser:
er.async_remove(self.entities["camera"].entity_id) er.async_remove(self.entities["camera"].entity_id)
del self.entities["camera"] del self.entities["camera"]
hass.create_task(
self.send( self.send(
None, browserEntities={k: v.entity_id for k, v in self.entities.items()} None, browserEntities={k: v.entity_id for k, v in self.entities.items()}
) )
)
def send(self, command, **kwargs): @callback
async def send(self, command, **kwargs):
"""Send a command to this browser.""" """Send a command to this browser."""
if self.connection is None: if self.connection is None:
return return
@@ -163,6 +167,16 @@ class BrowserModBrowser:
device = dr.async_get_device({(DOMAIN, self.browserID)}) device = dr.async_get_device({(DOMAIN, self.browserID)})
dr.async_remove_device(device.id) dr.async_remove_device(device.id)
def get_device_id(self, hass):
er = entity_registry.async_get(hass)
entities = list(self.entities.values())
if len(entities):
entity = entities[0]
entry = er.async_get(entity.entity_id)
if entry:
return entry.device_id
return "default"
@property @property
def connection(self): def connection(self):
"""The current websocket connections for this Browser.""" """The current websocket connections for this Browser."""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,8 @@ from homeassistant.components.websocket_api import (
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import callback
from .const import ( from .const import (
BROWSER_ID, BROWSER_ID,
DATA_STORE, DATA_STORE,
@@ -40,6 +42,7 @@ async def async_setup_connection(hass):
browserID = msg[BROWSER_ID] browserID = msg[BROWSER_ID]
store = hass.data[DOMAIN][DATA_STORE] store = hass.data[DOMAIN][DATA_STORE]
@callback
def send_update(data): def send_update(data):
connection.send_message(event_message(msg["id"], {"result": data})) connection.send_message(event_message(msg["id"], {"result": data}))
@@ -59,7 +62,9 @@ async def async_setup_connection(hass):
dev.update_settings(hass, store.get_browser(browserID).asdict()) dev.update_settings(hass, store.get_browser(browserID).asdict())
dev.open_connection(connection, msg["id"]) dev.open_connection(connection, msg["id"])
await store.set_browser( await store.set_browser(
browserID, last_seen=datetime.now(tz=timezone.utc).isoformat() browserID,
last_seen=datetime.now(tz=timezone.utc).isoformat(),
meta=dev.get_device_id(hass),
) )
send_update(store.asdict()) send_update(store.asdict())

View File

@@ -40,8 +40,8 @@ class BrowserModLight(BrowserModEntity, LightEntity):
def brightness(self): def brightness(self):
return self._data.get("screen_brightness", 1) return self._data.get("screen_brightness", 1)
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
self.browser.send("screen_on", **kwargs) await self.browser.send("screen_on", **kwargs)
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
self.browser.send("screen_off") await self.browser.send("screen_off")

View File

@@ -5,7 +5,7 @@
"dependencies": ["panel_custom", "websocket_api", "http", "frontend", "lovelace"], "dependencies": ["panel_custom", "websocket_api", "http", "frontend", "lovelace"],
"codeowners": [], "codeowners": [],
"requirements": [], "requirements": [],
"version": "2.0.0b4", "version": "2.0.0",
"iot_class": "local_push", "iot_class": "local_push",
"config_flow": true "config_flow": true
} }

View File

@@ -108,11 +108,11 @@ class BrowserModPlayer(BrowserModEntity, MediaPlayerEntity):
def media_position_updated_at(self): def media_position_updated_at(self):
return dt.utcnow() return dt.utcnow()
def set_volume_level(self, volume): async def async_set_volume_level(self, volume):
self.browser.send("player-set-volume", volume_level=volume) await self.browser.send("player-set-volume", volume_level=volume)
def mute_volume(self, mute): async def async_mute_volume(self, mute):
self.browser.send("player-mute", mute=mute) await self.browser.send("player-mute", mute=mute)
async def async_play_media(self, media_type, media_id, **kwargs): async def async_play_media(self, media_type, media_id, **kwargs):
if media_source.is_media_source_id(media_id): if media_source.is_media_source_id(media_id):
@@ -124,7 +124,7 @@ class BrowserModPlayer(BrowserModEntity, MediaPlayerEntity):
media_id = async_process_play_media_url(self.hass, media_id) media_id = async_process_play_media_url(self.hass, media_id)
if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC):
media_id = async_process_play_media_url(self.hass, media_id) media_id = async_process_play_media_url(self.hass, media_id)
self.browser.send( await self.browser.send(
"player-play", media_content_id=media_id, media_type=media_type, **kwargs "player-play", media_content_id=media_id, media_type=media_type, **kwargs
) )
@@ -136,20 +136,20 @@ class BrowserModPlayer(BrowserModEntity, MediaPlayerEntity):
# content_filter=lambda item: item.media_content_type.startswith("audio/"), # content_filter=lambda item: item.media_content_type.startswith("audio/"),
) )
def media_play(self): async def async_media_play(self):
self.browser.send("player-play") await self.browser.send("player-play")
def media_pause(self): async def async_media_pause(self):
self.browser.send("player-pause") await self.browser.send("player-pause")
def media_stop(self): async def async_media_stop(self):
self.browser.send("player-stop") await self.browser.send("player-stop")
def media_seek(self, position): async def async_media_seek(self, position):
self.browser.send("player-seek", position=position) await self.browser.send("player-seek", position=position)
def turn_off(self): async def async_turn_off(self):
self.browser.send("player-turn-off") await self.browser.send("player-turn-off")
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
self.browser.send("player-turn-on", **kwargs) await self.browser.send("player-turn-on", **kwargs)

View File

@@ -27,7 +27,7 @@ async def async_setup_services(hass):
if target not in browsers: if target not in browsers:
continue continue
browser = browsers[target] browser = browsers[target]
browser.send(service, **data) hass.create_task(browser.send(service, **data))
def handle_service(call): def handle_service(call):
service = call.service service = call.service

View File

@@ -16,6 +16,7 @@ class SettingsStoreData:
defaultPanel = attr.ib(type=str, default=None) defaultPanel = attr.ib(type=str, default=None)
sidebarPanelOrder = attr.ib(type=list, default=None) sidebarPanelOrder = attr.ib(type=list, default=None)
sidebarHiddenPanels = attr.ib(type=list, default=None) sidebarHiddenPanels = attr.ib(type=list, default=None)
sidebarTitle = attr.ib(type=str, default=None)
faviconTemplate = attr.ib(type=str, default=None) faviconTemplate = attr.ib(type=str, default=None)
titleTemplate = attr.ib(type=str, default=None) titleTemplate = attr.ib(type=str, default=None)

View File

@@ -2,7 +2,7 @@
## This browser ## This browser
A basic concept for Browser Mod is the _Browser_. A _Browser_ is identified by a unique `BrowserID` stored in the browsers [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API). The most important concept for Browser Mod is the _Browser_. A _Browser_ is identified by a unique `BrowserID` stored in the browsers [LocalStorage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API).
Browser Mod will initially assigning a random `BrowserID` to each _Browser_ that connects, but you can change this if you want. Browser Mod will initially assigning a random `BrowserID` to each _Browser_ that connects, but you can change this if you want.
@@ -43,15 +43,20 @@ If you are using [Home Assistant Cast](https://www.home-assistant.io/integration
This section is for settings that change the default behavior of the Home Assistant frontend. This section is for settings that change the default behavior of the Home Assistant frontend.
Each setting has three levels, _Global_, _Browser_ and _User_. For each option the first applicable value will be applied.
- Changes made on the _Global_ tab will be applied for everyone on every browser. In the screenshot below, for example, the sidebar title would be set to "My home" - the GLOBAL setting - for any user on any browser (even unregistered). For any user logged in on the "kitchen-dashboard" browser, the sidebar title would instead be set to "FOOD", except for the user "dev" for whom the sidebar title would always be "DEV MODE".
- Changes made on the _Browser_ tab will be applied for this _Browser_. The settings here override any _Global_ settings. ![Example of a frontend setting being applied for a user, a browser and globally](https://user-images.githubusercontent.com/1299821/187984798-04e72fff-7cce-4394-ba69-42e62c5e0acb.png)
- Changes made on the _User_ tab will be applied for the user you're currently logged in as - on any device. The settings here override any _Global_ or _Browser_ settings.
Note that if a setting is set at a lower level but _cleared_ on a higher, it is not _undone_. It's just not overridden. ### Title template
Also note that _User_ level settings can only be made when logged in as the user in question, and that the Browser Mod configuration panel is only available to administrators. If you need to change a setting for a non-admin user, you will need to temporarily make them admin for the setup, and then un-admin them. This allows you to set and dynamically update the title text of the browser tab/window by means on a Jinja [template](https://www.home-assistant.io/docs/configuration/templating/).
> Ex:
>
> ```jinja
> {{ states.persistent_notification | list | count}} - Home Assistant
> ```
### Favicon template ### Favicon template
@@ -67,17 +72,8 @@ This allows you to set and dynamically update the favicon of the browser tab/win
> {% endif %} > {% endif %}
> ``` > ```
Note that this _only_ applies to the current favicon of the page, not any manifest icons such as the loading icon or the icon you get if you save the page to your smartphones homescreen. For those, please see the [hass-favicon](https://github.com/thomasloven/hass-favicon) integration. Note that this _only_ applies to the current favicon of the page, not any manifest icons such as the loading icon or the icon you get if you save the page to your smartphones homescreen. For those, please see the [hass-favicon](https://github.com/thomasloven/hass-favicon) custom integration.
### Title template
This allows you to set and dynamically update the title text of the browser tab/window by means on a Jinja [template](https://www.home-assistant.io/docs/configuration/templating/).
> Ex:
>
> ```jinja
> {{ states.persistent_notification | list | count}} - Home Assistant
> ```
### Hide Sidebar ### Hide Sidebar
@@ -91,12 +87,23 @@ This will hide the header bar. Completely. It does not care if there are useful
> Tip: See the big yellow warning box at the top of this card? For some reason, it seems to be really easy to forget you turned this on. Please do not bother the Home Assistant team about the header bar missing if you have hidden it yourself. Really, I've forgotten multiple times myself. > Tip: See the big yellow warning box at the top of this card? For some reason, it seems to be really easy to forget you turned this on. Please do not bother the Home Assistant team about the header bar missing if you have hidden it yourself. Really, I've forgotten multiple times myself.
### Sidebar order
Did you know that you can change the order and hide items from the sidebar? To do so, either go into your profile settings at the bottom left and click "Change the order and hide items from the sidebar", or click and hold on the "Home Assistant" text at the top of the sidebar.
Normally, the order and hidden items only applies to the current device, but this will make it persistent according to the levels described above.
### Default dashboard ### Default dashboard
Like the Sidebar order, the default dashboard (the page shown when you simply access `https://<your home assistant url>/` with nothing after the `/`) can be set in your profile settings but only applies to the current device. This fixes that. Set the default dashboard that is shown when you access `https://<your home assistant url>/` with nothing after the `/`.
> *Note:* This also of works with other pages than lovelace dashboards, like e.g. `logbook` or even `history?device_id=f112fd806f2520c76318406f98cd244e&start_date=2022-09-02T16%3A00%3A00.000Z&end_date=2022-09-02T19%3A00%3A00.000Z`.
### Sidebar order
Set the order and hidden items of the sidebar. To change this setting:
- Click the "EDIT" button
- Change the sidebar to how you want it
- DO NOT click "DONE"
- Either add a new User or Browser setting or click one of the pencil icons to overwrite an old layout
- Click the "RESTORE" button
### Sidebar title
This changes the "Home Assistant" text that is displayed at the top of the sidebar.
Accepts Jinja [templates](https://www.home-assistant.io/docs/configuration/templating/).

View File

@@ -10,17 +10,17 @@ data:
left_button: Left button left_button: Left button
``` ```
![Dialog Anatomy](https://user-images.githubusercontent.com/1299821/182708739-f89e3b2b-199f-43e0-bf04-e1dfc7075b2a.png) ![Screenshot illustrating the title, content and button placements of a popup](https://user-images.githubusercontent.com/1299821/182708739-f89e3b2b-199f-43e0-bf04-e1dfc7075b2a.png)
## Size ## Size
The `size` parameter can be set to `normal`, `wide` and `fullscreen` with results as below (background blur has been exagerated for clarity): The `size` parameter can be set to `normal`, `wide` and `fullscreen` with results as below (background blur has been exagerated for clarity):
![Normal Size](https://user-images.githubusercontent.com/1299821/182709146-439814f1-d479-4fc7-aab1-e28f5c9a13c7.png) ![Screenshot of a normal size popup](https://user-images.githubusercontent.com/1299821/182709146-439814f1-d479-4fc7-aab1-e28f5c9a13c7.png)
![Wide Size](https://user-images.githubusercontent.com/1299821/182709172-c98a9c23-5e58-4564-bcb7-1d187842948f.png) ![Screenshot of a wide size popup](https://user-images.githubusercontent.com/1299821/182709172-c98a9c23-5e58-4564-bcb7-1d187842948f.png)
![Fullscreen Size](https://user-images.githubusercontent.com/1299821/182709224-fb2e7b92-8a23-4422-95a0-f0f2835909e0.png) ![Screenshot of a fullscreen size popup](https://user-images.githubusercontent.com/1299821/182709224-fb2e7b92-8a23-4422-95a0-f0f2835909e0.png)
## HTML content ## HTML content
@@ -34,7 +34,7 @@ data:
<p> Pretty much any HTML works: <ha-icon icon="mdi:lamp" style="color: red;"></ha-icon> <p> Pretty much any HTML works: <ha-icon icon="mdi:lamp" style="color: red;"></ha-icon>
``` ```
![HTML content](https://user-images.githubusercontent.com/1299821/182710044-6fea3ba3-5262-4361-a131-691770340518.png) ![Screenshot of a popup rendering the HTML code above](https://user-images.githubusercontent.com/1299821/182710044-6fea3ba3-5262-4361-a131-691770340518.png)
## Dashboard card content ## Dashboard card content
@@ -50,7 +50,7 @@ data:
- light.kitchen_lights - light.kitchen_lights
``` ```
![Card content](https://user-images.githubusercontent.com/1299821/182710445-f09b74b8-dd53-4d65-8eba-0945fc1d418e.png) ![Screenshot of a popup rendering the entities card described above](https://user-images.githubusercontent.com/1299821/182710445-f09b74b8-dd53-4d65-8eba-0945fc1d418e.png)
## Form content ## Form content
`content` can be a list of ha-form schemas and the popup will then contain a form for user input: `content` can be a list of ha-form schemas and the popup will then contain a form for user input:
@@ -91,7 +91,7 @@ data:
slider: true slider: true
``` ```
![Form content](https://user-images.githubusercontent.com/1299821/182712670-f3b4fdb7-84a9-49d1-a26f-2cdaa450fa0e.png) ![Screenshot of a popup rendering the form described above](https://user-images.githubusercontent.com/1299821/182712670-f3b4fdb7-84a9-49d1-a26f-2cdaa450fa0e.png)
## Actionable popups ## Actionable popups
@@ -127,7 +127,7 @@ data:
entity_id: light.bed_light entity_id: light.bed_light
``` ```
![Multi-level popup](https://user-images.githubusercontent.com/1299821/182713421-708d0026-bcfa-4ba6-bbcd-3b85b584162d.gif) ![Animated screenshot of a popup which opens other popups when one of the action buttons are pressed](https://user-images.githubusercontent.com/1299821/182713421-708d0026-bcfa-4ba6-bbcd-3b85b584162d.gif)
## Forward form data ## Forward form data
@@ -158,4 +158,4 @@ data:
value: 12 value: 12
``` ```
![Vacuum popup](https://user-images.githubusercontent.com/1299821/182713714-ef4149b1-217a-4d41-9737-714f5320c25c.png) ![Screenshot of a popup allowing the user to choose which rooms to vacuum](https://user-images.githubusercontent.com/1299821/182713714-ef4149b1-217a-4d41-9737-714f5320c25c.png)

View File

@@ -67,6 +67,36 @@ script:
Will print `"Button was clicked in 79be65e8-f06c78f" to the Home Assistant log. Will print `"Button was clicked in 79be65e8-f06c78f" to the Home Assistant log.
# Calling services
Services can be called from the backend using the normal service call procedures. Registered Browsers can be selected as targets through their device:
![A picture exemplifying setting up a browser_mod.more_info service call in the GUI editor](https://user-images.githubusercontent.com/1299821/180668350-1cbe751d-615d-4102-b939-e49e9cd2ca74.png)
In yaml, the BrowserID can be used for targeting a specific browser:
```yaml
service: browser_mod.more_info
data:
entity: light.bed_light
browser_id:
- 79be65e8-f06c78f
```
If no target or `browser_id` is specified, the service will target all registerd Browsers.
To call a service from a dashboard use the call-service [action](https://www.home-assistant.io/dashboards/actions/) or the special action `fire-dom-event`:
```yaml
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.more_info
data:
entity: light.bed_light
```
Services called via `fire-dom-event` or called as a part of a different service call will (by default) _only_ target the current Browser (even if it's not registered).
# Browser Mod Services # Browser Mod Services

View File

@@ -0,0 +1,294 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators.js";
import { selectTree } from "../helpers";
class BrowserModSettingsTable extends LitElement {
@property() settingKey;
@property() settingSelector = {
template: {},
};
@property() hass;
@property() default;
@property() tableData = [];
_users = undefined;
firstUpdated() {
window.browser_mod.addEventListener("browser-mod-config-update", () =>
this.updateTable()
);
}
updated(changedProperties) {
if (changedProperties.has("settingKey")) this.updateTable();
if (
changedProperties.has("hass") &&
changedProperties.get("hass") === undefined
)
this.updateTable();
}
async fetchUsers(): Promise<any[]> {
if (this._users === undefined)
this._users = await this.hass.callWS({ type: "config/auth/list" });
return this._users;
}
clearSetting(type, target) {
const clearSettingCallback = async () => {
if (this.settingKey === "sidebarPanelOrder") {
const sideBar: any = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar"
);
window.browser_mod.setSetting(type, target, {
sidebarHiddenPanels: "[]",
sidebarPanelOrder: "[]",
});
window.browser_mod.setSetting(type, target, {
sidebarHiddenPanels: undefined,
sidebarPanelOrder: undefined,
});
return;
}
if (this.default)
window.browser_mod.setSetting(type, target, {
[this.settingKey]: this.default,
});
window.browser_mod.setSetting(type, target, {
[this.settingKey]: undefined,
});
};
window.browser_mod?.showPopup(
"Are you sure",
"Do you wish to clear this setting?",
{
right_button: "Yes",
right_button_action: clearSettingCallback,
left_button: "No",
}
);
}
changeSetting(type, target) {
const changeSettingCallback = async (newValue) => {
if (this.settingKey === "sidebarPanelOrder") {
const sideBar: any = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar"
);
window.browser_mod.setSetting(type, target, {
sidebarHiddenPanels: JSON.stringify(sideBar._hiddenPanels),
sidebarPanelOrder: JSON.stringify(sideBar._panelOrder),
});
console.log(sideBar._hiddenPanels, sideBar._panelOrder);
return;
}
let value = newValue.value;
window.browser_mod.setSetting(type, target, { [this.settingKey]: value });
};
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const def =
(type === "global" ? settings.global : settings[type][target]) ??
this.default;
window.browser_mod?.showPopup(
"Change value",
(this.settingSelector as any).plaintext ?? [
{
name: "value",
label: (this.settingSelector as any).label ?? "",
default: def,
selector: this.settingSelector,
},
],
{
right_button: "OK",
right_button_action: changeSettingCallback,
left_button: "Cancel",
}
);
}
addBrowserSetting() {
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const allBrowsers = window.browser_mod._data.browsers;
const browsers = [];
for (const target of Object.keys(allBrowsers)) {
if (settings.browser[target] == null) browsers.push(target);
}
if (browsers.length === 0) {
window.browser_mod.showPopup(
"No browsers to configure",
"All registered browsers have already been configured.",
{ right_button: "OK" }
);
return;
}
window.browser_mod.showPopup(
"Select browser to configure",
[
{
name: "browser",
label: "",
selector: {
select: { options: browsers },
},
},
],
{
right_button: "Next",
right_button_action: (value) =>
this.changeSetting("browser", value.browser),
left_button: "Cancel",
}
);
}
async addUserSetting() {
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const allUsers = await this.fetchUsers();
const users = [];
for (const target of allUsers) {
if (target.username && settings.user[target.id] == null)
users.push({ label: target.name, value: target.id });
}
if (users.length === 0) {
window.browser_mod.showPopup(
"No users to configure",
"All users have already been configured.",
{ right_button: "OK" }
);
return;
}
window.browser_mod.showPopup(
"Select user to configure",
[
{
name: "user",
label: "",
selector: {
select: { options: users },
},
},
],
{
right_button: "Next",
right_button_action: (value) => this.changeSetting("user", value.user),
left_button: "Cancel",
}
);
}
async updateTable() {
if (this.hass === undefined) return;
const users = await this.fetchUsers();
const settings = window.browser_mod?.getSetting?.(this.settingKey);
const data = [];
for (const [k, v] of Object.entries(settings.user)) {
const user = users.find((usr) => usr.id === k);
data.push({
name: `User: ${user.name}`,
value: String(v),
controls: html`
<ha-icon-button @click=${() => this.changeSetting("user", k)}>
<ha-icon .icon=${"mdi:pencil"} style="display:flex;"></ha-icon>
</ha-icon-button>
<ha-icon-button @click=${() => this.clearSetting("user", k)}>
<ha-icon .icon=${"mdi:delete"} style="display:flex;"></ha-icon>
</ha-icon-button>
`,
});
}
data.push({
name: "",
value: html`
<mwc-button @click=${() => this.addUserSetting()}>
<ha-icon .icon=${"mdi:plus"}></ha-icon>
Add user setting
</mwc-button>
`,
});
for (const [k, v] of Object.entries(settings.browser)) {
data.push({
name: `Browser: ${k}`,
value: String(v),
controls: html`
<ha-icon-button @click=${() => this.changeSetting("browser", k)}>
<ha-icon .icon=${"mdi:pencil"} style="display:flex;"></ha-icon>
</ha-icon-button>
<ha-icon-button @click=${() => this.clearSetting("browser", k)}>
<ha-icon .icon=${"mdi:delete"} style="display:flex;"></ha-icon>
</ha-icon-button>
`,
});
}
data.push({
name: "",
value: html`
<mwc-button @click=${() => this.addBrowserSetting()}>
<ha-icon .icon=${"mdi:plus"}></ha-icon>
Add browser setting
</mwc-button>
`,
});
data.push({
name: "GLOBAL",
value:
settings.global != null
? String(settings.global)
: html`<span style="color: var(--warning-color);">DEFAULT</span>`,
controls: html`
<ha-icon-button @click=${() => this.changeSetting("global", null)}>
<ha-icon .icon=${"mdi:pencil"} style="display:flex;"></ha-icon>
</ha-icon-button>
<ha-icon-button @click=${() => this.clearSetting("global", null)}>
<ha-icon .icon=${"mdi:delete"} style="display:flex;"></ha-icon>
</ha-icon-button>
`,
});
this.tableData = data;
}
render() {
const global = window.browser_mod?.global_settings?.[this.settingKey];
const columns = {
name: {
title: "Name",
grows: true,
},
value: {
title: "Value",
grows: true,
},
controls: {},
};
return html`
<ha-data-table .columns=${columns} .data=${this.tableData} auto-height>
</ha-data-table>
`;
}
static get styles() {
return css`
:host {
display: block;
}
`;
}
}
customElements.define("browser-mod-settings-table", BrowserModSettingsTable);

View File

@@ -136,7 +136,7 @@ class BrowserModRegisteredBrowsersCard extends LitElement {
private _renderInteractionAlert() { private _renderInteractionAlert() {
return html` return html`
<ha-alert title="Interaction requirement"> <ha-alert title="Interaction requirement">
For security reasons many browsers require the user to interact with a For privacy reasons many browsers require the user to interact with a
webpage before allowing audio playback or video capture. This may affect webpage before allowing audio playback or video capture. This may affect
the the
<code>media_player</code> and <code>camera</code> components of Browser <code>media_player</code> and <code>camera</code> components of Browser

View File

@@ -1,38 +1,73 @@
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { property, state } from "lit/decorators.js"; import { property, state } from "lit/decorators.js";
import { loadDeveloperToolsTemplate } from "../helpers"; import { loadDeveloperToolsTemplate, selectTree } from "../helpers";
import "./browser-mod-settings-table";
loadDeveloperToolsTemplate(); loadDeveloperToolsTemplate();
class BrowserModFrontendSettingsCard extends LitElement { class BrowserModFrontendSettingsCard extends LitElement {
@property() hass; @property() hass;
@state() _selectedTab = 0; @state() _dashboards = [];
@state() _editSidebar = false;
_savedSidebar = { panelOrder: [], hiddenPanels: [] };
firstUpdated() { firstUpdated() {
window.browser_mod.addEventListener("browser-mod-config-update", () => window.browser_mod.addEventListener("browser-mod-config-update", () =>
this.requestUpdate() this.requestUpdate()
); );
window.browser_mod.addEventListener("browser-mod-favicon-update", () =>
this.requestUpdate()
);
} }
_handleSwitchTab(ev: CustomEvent) { updated(changedProperties) {
this._selectedTab = parseInt(ev.detail.index, 10); if (
changedProperties.has("hass") &&
changedProperties.get("hass") === undefined
) {
(async () =>
(this._dashboards = await this.hass.callWS({
type: "lovelace/dashboards/list",
})))();
}
}
async toggleEditSidebar() {
const sideBar: any = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar"
);
sideBar.editMode = !sideBar.editMode;
this._editSidebar = sideBar.editMode;
if (this._editSidebar) {
this._savedSidebar = {
panelOrder: sideBar._panelOrder,
hiddenPanels: sideBar._hiddenPanels,
};
} else {
sideBar._panelOrder = this._savedSidebar.panelOrder ?? [];
sideBar._hiddenPanels = this._savedSidebar.hiddenPanels ?? [];
this._savedSidebar = { panelOrder: [], hiddenPanels: [] };
}
} }
render() { render() {
const level = ["user", "browser", "global"][this._selectedTab]; const db = this._dashboards.map((d) => {
return { value: d.url_path, label: d.title };
});
const dashboardSelector = {
select: {
options: [{ value: "lovelace", label: "lovelace (default)" }, ...db],
custom_value: true,
},
};
return html` return html`
<ha-card header="Frontend Settings" outlined> <ha-card header="Frontend Settings" outlined>
<div class="card-content"> <div class="card-content">
<ha-alert alert-type="warning"> <ha-alert alert-type="warning" title="Please note:">
<p> The settings in this section severely change the way the Home
Please note: The settings in this section severely change the way the Home
Assistant frontend works and looks. It is very easy to forget that Assistant frontend works and looks. It is very easy to forget that
you made a setting here when you switch devices or user. you made a setting here when you switch devices or user.
</p>
<p> <p>
Do not report any issues to Home Assistant before clearing Do not report any issues to Home Assistant before clearing
<b>EVERY</b> setting here and thouroghly clearing all your browser <b>EVERY</b> setting here and thouroghly clearing all your browser
@@ -41,234 +76,119 @@ class BrowserModFrontendSettingsCard extends LitElement {
</p> </p>
</ha-alert> </ha-alert>
<p> <p>
Global settings are applied for all users and browsers.</br> Settings below are applied by first match. I.e. if a matching User
User settings are applied to the current user and overrides any Global settings.</br> setting exists, it will be applied. Otherwise any matching Browser
Browser settings are applied for the current browser and overrides any User or Global settings. setting and otherwise the GLOBAL setting if that differs from
DEFAULT.
</p> </p>
<mwc-tab-bar
.activeIndex=${this._selectedTab}
@MDCTabBar:activated=${this._handleSwitchTab}
>
<mwc-tab .label=${"User (" + this.hass.user.name + ")"}></mwc-tab>
<ha-icon .icon=${"mdi:chevron-double-right"}></ha-icon>
<mwc-tab .label=${"Browser"}></mwc-tab>
<ha-icon .icon=${"mdi:chevron-double-right"}></ha-icon>
<mwc-tab .label=${"Global"}></mwc-tab>
</mwc-tab-bar>
${this._render_settings(level)}
</div>
</ha-card>
`;
}
_render_settings(level) {
const global = window.browser_mod.global_settings;
const browser = window.browser_mod.browser_settings;
const user = window.browser_mod.user_settings;
const current = { global, browser, user }[level];
const DESC_BOOLEAN = (val) =>
({ true: "Enabled", false: "Disabled", undefined: "Unset" }[String(val)]);
const DESC_SET_UNSET = (val) => (val === undefined ? "Unset" : "Set");
const OVERRIDDEN = (key) => {
if (level !== "browser" && browser[key] !== undefined)
return html`<br />Overridden by browser setting`;
if (level === "global" && user[key] !== undefined)
return html`<br />Overridden by user setting`;
};
return html`
<div class="box">
<ha-settings-row>
<span slot="heading">Favicon template</span>
${OVERRIDDEN("faviconTemplate")}
<img src="${window.browser_mod._currentFavicon}" class="favicon" />
</ha-settings-row>
<ha-code-editor
.hass=${this.hass}
.value=${current.faviconTemplate}
@value-changed=${(ev) => {
const tpl = ev.detail.value || undefined;
window.browser_mod.set_setting("faviconTemplate", tpl, level);
}}
></ha-code-editor>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting(
"faviconTemplate",
undefined,
level
)}
>
Clear
</mwc-button>
</ha-settings-row>
<div class="separator"></div> <div class="separator"></div>
<ha-settings-row> <ha-settings-row>
<span slot="heading">Title template</span> <span slot="heading">Title template</span>
${OVERRIDDEN("titleTemplate")} <span slot="description">
Jinja template for the browser window/tab title
</span>
</ha-settings-row> </ha-settings-row>
<ha-code-editor <browser-mod-settings-table
.hass=${this.hass} .hass=${this.hass}
.value=${current.titleTemplate} .settingKey=${"titleTemplate"}
@value-changed=${(ev) => { ></browser-mod-settings-table>
const tpl = ev.detail.value || undefined;
window.browser_mod.set_setting("titleTemplate", tpl, level);
}}
></ha-code-editor>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("titleTemplate", undefined, level)}
>
Clear
</mwc-button>
</ha-settings-row>
<div class="separator"></div> <div class="separator"></div>
<ha-settings-row> <ha-settings-row>
<span slot="heading">Hide Sidebar</span> <span slot="heading">Favicon template</span>
<span slot="description">Hide the sidebar and hamburger menu</span> <span slot="description">
Currently: ${DESC_BOOLEAN(current.hideSidebar)} Jinja template for the browser favicon
${OVERRIDDEN("hideSidebar")} </span>
</ha-settings-row>
<ha-settings-row>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideSidebar", true, level)}
>
Enable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideSidebar", false, level)}
>
Disable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideSidebar", undefined, level)}
>
Clear
</mwc-button>
</ha-settings-row> </ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"faviconTemplate"}
></browser-mod-settings-table>
<div class="separator"></div> <div class="separator"></div>
<ha-settings-row> <ha-settings-row>
<span slot="heading">Hide Header</span> <span slot="heading">Hide sidebar</span>
<span slot="description">Hide the header on all pages</span> <span slot="description">
Currently: ${DESC_BOOLEAN(current.hideHeader)} Completely remove the sidebar from all panels
${OVERRIDDEN("hideHeader")} </span>
</ha-settings-row> </ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"hideSidebar"}
.settingSelector=${{ boolean: {}, label: "Hide sidebar" }}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row> <ha-settings-row>
<mwc-button <span slot="heading">Hide header</span>
@click=${() => <span slot="description">
window.browser_mod.set_setting("hideHeader", true, level)} Completely remove the header from all panels
> </span>
Enable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideHeader", false, level)}
>
Disable
</mwc-button>
<mwc-button
@click=${() =>
window.browser_mod.set_setting("hideHeader", undefined, level)}
>
Clear
</mwc-button>
</ha-settings-row> </ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"hideHeader"}
.settingSelector=${{ boolean: {}, label: "Hide header" }}
></browser-mod-settings-table>
<div class="separator"></div>
<ha-settings-row>
<span slot="heading">Default dashboard</span>
<span slot="description">
The dashboard that is showed when navigating to
${location.origin}/
</span>
</ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"defaultPanel"}
.settingSelector=${dashboardSelector}
.default=${"lovelace"}
></browser-mod-settings-table>
<div class="separator"></div> <div class="separator"></div>
<ha-settings-row> <ha-settings-row>
<span slot="heading">Sidebar order</span> <span slot="heading">Sidebar order</span>
<span slot="description"> <span slot="description">
Order and visibility of sidebar buttons Order and visibility of sidebar items. <br />Click EDIT and set
the sidebar up as you want. Then save the settings and finally
click RESTORE.
</span> </span>
Currently: ${DESC_SET_UNSET(current.sidebarPanelOrder)} <mwc-button @click=${() => this.toggleEditSidebar()}>
${OVERRIDDEN("sidebarPanelOrder")} ${this._editSidebar ? "Restore" : "Edit"}
</ha-settings-row>
<ha-settings-row>
<span slot="description">
Clearing this does NOT restore the original default order.
</span>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"sidebarPanelOrder",
localStorage.getItem("sidebarPanelOrder"),
level
);
window.browser_mod.set_setting(
"sidebarHiddenPanels",
localStorage.getItem("sidebarHiddenPanels"),
level
);
}}
>
Set
</mwc-button>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"sidebarPanelOrder",
undefined,
level
);
window.browser_mod.set_setting(
"sidebarHiddenPanels",
undefined,
level
);
}}
>
Clear
</mwc-button> </mwc-button>
</ha-settings-row> </ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"sidebarPanelOrder"}
.settingSelector=${{
plaintext: "Press OK to store the current sidebar order",
}}
.default=${"lovelace"}
></browser-mod-settings-table>
<div class="separator"></div> <div class="separator"></div>
<ha-settings-row> <ha-settings-row>
<span slot="heading">Default dashboard</span> <span slot="heading">Sidebar title</span>
<span slot="description"
>The dashboard that's displayed by default</span
>
Currently: ${DESC_SET_UNSET(current.defaultPanel)}
${OVERRIDDEN("defaultPanel")}
</ha-settings-row>
<ha-settings-row>
<span slot="description"> <span slot="description">
Clearing this does NOT restore the original default dashboard. The title at the top of the sidebar
</span> </span>
<mwc-button
@click=${() => {
window.browser_mod.set_setting(
"defaultPanel",
localStorage.getItem("defaultPanel"),
level
);
}}
>
Set
</mwc-button>
<mwc-button
@click=${() => {
window.browser_mod.set_setting("defaultPanel", undefined, level);
}}
>
Clear
</mwc-button>
</ha-settings-row> </ha-settings-row>
<browser-mod-settings-table
.hass=${this.hass}
.settingKey=${"sidebarTitle"}
.settingSelector=${{ text: {} }}
></browser-mod-settings-table>
</div> </div>
</ha-card>
`; `;
} }
@@ -280,7 +200,7 @@ class BrowserModFrontendSettingsCard extends LitElement {
} }
.separator { .separator {
border-bottom: 1px solid var(--divider-color); border-bottom: 1px solid var(--divider-color);
margin: 0 -8px; margin: 16px -16px 0px;
} }
img.favicon { img.favicon {
width: 64px; width: 64px;

View File

@@ -15,12 +15,13 @@ loadConfigDashboard().then(() => {
@property() connection; @property() connection;
firstUpdated() { firstUpdated() {
window.browser_mod.addEventListener("browser-mod-config-update", () => window.addEventListener("browser-mod-config-update", () =>
this.requestUpdate() this.requestUpdate()
); );
} }
render() { render() {
if (!window.browser_mod) return html``;
return html` return html`
<ha-app-layout> <ha-app-layout>
<app-header slot="header" fixed> <app-header slot="header" fixed>

View File

@@ -46,21 +46,31 @@ class BrowserModRegisteredBrowsersCard extends LitElement {
return html` return html`
<ha-card header="Registered Browsers" outlined> <ha-card header="Registered Browsers" outlined>
<div class="card-content"> <div class="card-content">
${Object.keys(window.browser_mod.browsers).map( ${Object.keys(window.browser_mod.browsers).map((d) => {
(d) => html` <ha-settings-row> const browser = window.browser_mod.browsers[d];
return html` <ha-settings-row>
<span slot="heading"> ${d} </span> <span slot="heading"> ${d} </span>
<span slot="description"> <span slot="description">
Last connected: Last connected:
<ha-relative-time <ha-relative-time
.hass=${this.hass} .hass=${this.hass}
.datetime=${window.browser_mod.browsers[d].last_seen} .datetime=${browser.last_seen}
></ha-relative-time> ></ha-relative-time>
</span> </span>
${browser.meta && browser.meta !== "default"
? html`
<a href="config/devices/device/${browser.meta}">
<ha-icon-button>
<ha-icon .icon=${"mdi:devices"}></ha-icon>
</ha-icon-button>
</a>
`
: ""}
<ha-icon-button .browserID=${d} @click=${this.unregister_browser}> <ha-icon-button .browserID=${d} @click=${this.unregister_browser}>
<ha-icon .icon=${"mdi:delete"}></ha-icon> <ha-icon .icon=${"mdi:delete"}></ha-icon>
</ha-icon-button> </ha-icon-button>
</ha-settings-row>` </ha-settings-row>`;
)} })}
</div> </div>
${window.browser_mod.browsers["CAST"] === undefined ${window.browser_mod.browsers["CAST"] === undefined
? html` ? html`
@@ -79,6 +89,7 @@ class BrowserModRegisteredBrowsersCard extends LitElement {
return css` return css`
ha-icon-button > * { ha-icon-button > * {
display: flex; display: flex;
color: var(--primary-text-color);
} }
`; `;
} }

View File

@@ -1,9 +1,17 @@
const TIMEOUT_ERROR = "SELECTTREE-TIMEOUT"; const TIMEOUT_ERROR = "SELECTTREE-TIMEOUT";
async function _await_el(el) { export async function await_element(el, hard = false) {
if (el.localName?.includes("-")) if (el.localName?.includes("-"))
await customElements.whenDefined(el.localName); await customElements.whenDefined(el.localName);
if (el.updateComplete) await el.updateComplete; if (el.updateComplete) await el.updateComplete;
if (hard) {
if (el.pageRendered) await el.pageRendered;
if (el._panelState) {
let rounds = 0;
while (el._panelState !== "loaded" && rounds++ < 5)
await new Promise((r) => setTimeout(r, 100));
}
}
} }
async function _selectTree(root, path, all = false) { async function _selectTree(root, path, all = false) {
@@ -18,7 +26,7 @@ async function _selectTree(root, path, all = false) {
if (!p.trim().length) continue; if (!p.trim().length) continue;
_await_el(e); await_element(e);
el = p === "$" ? [e.shadowRoot] : e.querySelectorAll(p); el = p === "$" ? [e.shadowRoot] : e.querySelectorAll(p);
} }
return all ? el : el[0]; return all ? el : el[0];
@@ -102,6 +110,7 @@ export const loadConfigDashboard = async () => {
const configRouter: any = document.createElement("ha-panel-config"); const configRouter: any = document.createElement("ha-panel-config");
await configRouter?.routerOptions?.routes?.dashboard?.load?.(); // Load ha-config-dashboard await configRouter?.routerOptions?.routes?.dashboard?.load?.(); // Load ha-config-dashboard
await configRouter?.routerOptions?.routes?.cloud?.load?.(); // Load ha-settings-row await configRouter?.routerOptions?.routes?.cloud?.load?.(); // Load ha-settings-row
await configRouter?.routerOptions?.routes?.entities?.load?.(); // Load ha-data-table
await customElements.whenDefined("ha-config-dashboard"); await customElements.whenDefined("ha-config-dashboard");
}; };
@@ -132,3 +141,32 @@ export function throttle(timeout) {
}; };
}; };
} }
export function runOnce(restart = false) {
return function (target, propertyKey, descriptor) {
const fn = descriptor.value;
let running = undefined;
const newfn = function (...rest) {
if (restart && running === false) running = true;
if (running !== undefined) return;
running = false;
const retval = fn.bind(this)(...rest);
if (running) {
running = undefined;
return newfn.bind(this)(...rest);
} else {
running = undefined;
return retval;
}
};
descriptor.value = newfn;
};
}
export async function waitRepeat(fn, times, delay) {
while (times--) {
fn();
await new Promise((r) => setTimeout(r, delay));
}
}

View File

@@ -13,7 +13,7 @@ export const ConnectionMixin = (SuperClass) => {
public browserEntities = {}; public browserEntities = {};
LOG(...args) { LOG(...args) {
return; if (window.browser_mod_log === undefined) return;
const dt = new Date(); const dt = new Date();
console.log(`${dt.toLocaleTimeString()}`, ...args); console.log(`${dt.toLocaleTimeString()}`, ...args);
@@ -24,7 +24,7 @@ export const ConnectionMixin = (SuperClass) => {
} }
private fireEvent(event, detail = undefined) { private fireEvent(event, detail = undefined) {
this.dispatchEvent(new CustomEvent(event, { detail })); this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true }));
} }
private incoming_message(msg) { private incoming_message(msg) {
@@ -37,6 +37,7 @@ export const ConnectionMixin = (SuperClass) => {
this.update_config(msg.result); this.update_config(msg.result);
} }
this._connectionResolve?.(); this._connectionResolve?.();
this._connectionResolve = undefined;
} }
private update_config(cfg) { private update_config(cfg) {

View File

@@ -1,25 +1,38 @@
import { selectTree } from "../helpers"; import { await_element, waitRepeat, runOnce, selectTree } from "../helpers";
export const AutoSettingsMixin = (SuperClass) => { export const AutoSettingsMixin = (SuperClass) => {
return class AutoSettingsMixinClass extends SuperClass { class AutoSettingsMixinClass extends SuperClass {
_faviconTemplateSubscription; _faviconTemplateSubscription;
_titleTemplateSubscription; _titleTemplateSubscription;
_sidebarTitleSubscription;
__currentTitle = undefined; __currentTitle = undefined;
@runOnce()
async runHideHeader() {
while (!(await this._hideHeader()))
await new Promise((r) => setTimeout(r, 500));
}
@runOnce(true)
async runUpdateTitle() {
await waitRepeat(() => this._updateTitle(), 3, 500);
}
constructor() { constructor() {
super(); super();
this._auto_settings_setup(); const runUpdates = async () => {
this.addEventListener("browser-mod-config-update", () => this.runUpdateTitle();
this._auto_settings_setup() this.runHideHeader();
); };
window.addEventListener("location-changed", () => { this._auto_settings_setup();
this._updateTitle(); this.addEventListener("browser-mod-config-update", () => {
setTimeout(() => this._updateTitle(), 500); this._auto_settings_setup();
setTimeout(() => this._updateTitle(), 1000); runUpdates();
setTimeout(() => this._updateTitle(), 5000);
}); });
window.addEventListener("location-changed", runUpdates);
} }
async _auto_settings_setup() { async _auto_settings_setup() {
@@ -40,7 +53,7 @@ export const AutoSettingsMixin = (SuperClass) => {
// Default panel // Default panel
if (settings.defaultPanel) { if (settings.defaultPanel) {
localStorage.setItem("defaultPanel", settings.defaultPanel); localStorage.setItem("defaultPanel", `"${settings.defaultPanel}"`);
} }
// Hide sidebar // Hide sidebar
@@ -55,17 +68,23 @@ export const AutoSettingsMixin = (SuperClass) => {
).then((el) => el?.remove?.()); ).then((el) => el?.remove?.());
} }
// Hide header // Sidebar title
if (settings.hideHeader === true) { if (settings.sidebarTitle) {
customElements.whenDefined("app-header-layout").then(() => { (async () => {
const appHeader = customElements.get("app-header").prototype; if (this._sidebarTitleSubscription) {
const _attached = appHeader.attached; this._sidebarTitleSubscription();
appHeader.attached = function () {
_attached.bind(this)();
this.style.setProperty("display", "none");
};
});
} }
this._sidebarTitleSubscription = undefined;
this._sidebarTitleSubscription =
await this.connection.subscribeMessage(this._updateSidebarTitle, {
type: "render_template",
template: settings.sidebarTitle,
variables: {},
});
})();
}
// Hide header
// Favicon template // Favicon template
if (settings.faviconTemplate !== undefined) { if (settings.faviconTemplate !== undefined) {
@@ -103,6 +122,15 @@ export const AutoSettingsMixin = (SuperClass) => {
} }
} }
_updateSidebarTitle({ result }) {
selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout app-drawer ha-sidebar $ .title"
).then((el) => {
if (el) (el as HTMLElement).innerHTML = result;
});
}
get _currentFavicon() { get _currentFavicon() {
const link: any = document.head.querySelector("link[rel~='icon']"); const link: any = document.head.querySelector("link[rel~='icon']");
return link?.href; return link?.href;
@@ -111,7 +139,6 @@ export const AutoSettingsMixin = (SuperClass) => {
_updateFavicon({ result }) { _updateFavicon({ result }) {
const link: any = document.head.querySelector("link[rel~='icon']"); const link: any = document.head.querySelector("link[rel~='icon']");
link.href = result; link.href = result;
window.browser_mod.fireEvent("browser-mod-favicon-update");
} }
get _currentTitle() { get _currentTitle() {
@@ -121,7 +148,77 @@ export const AutoSettingsMixin = (SuperClass) => {
_updateTitle(data = undefined) { _updateTitle(data = undefined) {
if (data) this.__currentTitle = data.result; if (data) this.__currentTitle = data.result;
if (this.__currentTitle) document.title = this.__currentTitle; if (this.__currentTitle) document.title = this.__currentTitle;
window.browser_mod.fireEvent("browser-mod-favicon-update");
} }
};
async _hideHeader() {
if (this.settings.hideHeader !== true) return true;
let el = await selectTree(
document,
"home-assistant $ home-assistant-main $ app-drawer-layout partial-panel-resolver"
);
if (!el) return false;
let steps = 0;
while (el && el.localName !== "ha-app-layout" && steps++ < 5) {
await await_element(el, true);
const next =
el.querySelector("ha-app-layout") ??
el.firstElementChild ??
el.shadowRoot;
el = next;
}
if (el?.localName !== "ha-app-layout") return false;
if (el.header) {
el.header.style.setProperty("display", "none");
setTimeout(() => el._updateLayoutStates(), 0);
return true;
}
return false;
}
getSetting(key) {
const retval = { global: undefined, browser: {}, user: {} };
retval.global = this._data.settings?.[key];
for (const [k, v] of Object.entries(this._data.browsers ?? {})) {
if ((v as any).settings?.[key] != null)
retval.browser[k] = (v as any).settings[key];
}
for (const [k, v] of Object.entries(this._data.user_settings ?? {})) {
if (v[key] != null) retval.user[k] = v[key];
}
return retval;
}
setSetting(type, target, settings) {
if (type === "global") {
for (const [key, value] of Object.entries(settings))
this.connection.sendMessage({
type: "browser_mod/settings",
key,
value,
});
} else if (type === "browser") {
const browser = this._data.browsers[target];
const newsettings = { ...browser.settings, ...settings };
console.log(newsettings);
this.connection.sendMessage({
type: "browser_mod/register",
browserID: target,
data: {
...browser,
settings: newsettings,
},
});
} else if (type === "user") {
const user = target;
for (const [key, value] of Object.entries(settings))
this.connection.sendMessage({
type: "browser_mod/settings",
user,
key,
value,
});
}
}
}
return AutoSettingsMixinClass;
}; };

View File

@@ -28,6 +28,7 @@ class BrowserModPopup extends LitElement {
async closeDialog() { async closeDialog() {
this.open = false; this.open = false;
this.card = undefined;
clearInterval(this._timeoutTimer); clearInterval(this._timeoutTimer);
if (this._autocloseListener) { if (this._autocloseListener) {
window.browser_mod.removeEventListener( window.browser_mod.removeEventListener(
@@ -223,7 +224,6 @@ class BrowserModPopup extends LitElement {
static get styles() { static get styles() {
return css` return css`
ha-dialog { ha-dialog {
--dialog-backdrop-filter: blur(5px);
z-index: 10; z-index: 10;
--mdc-dialog-min-width: var(--popup-min-width, 400px); --mdc-dialog-min-width: var(--popup-min-width, 400px);
--mdc-dialog-max-width: var(--popup-max-width, 600px); --mdc-dialog-max-width: var(--popup-max-width, 600px);
@@ -320,10 +320,7 @@ class BrowserModPopup extends LitElement {
ha-dialog { ha-dialog {
--mdc-dialog-min-width: 100vw; --mdc-dialog-min-width: 100vw;
--mdc-dialog-max-width: 100vw; --mdc-dialog-max-width: 100vw;
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--mdc-shape-medium: 0px; --mdc-shape-medium: 0px;
--vertial-align-dialog: flex-end;
} }
} }
`; `;
@@ -345,6 +342,13 @@ export const PopupMixin = (SuperClass) => {
this._popupEl = document.createElement("browser-mod-popup"); this._popupEl = document.createElement("browser-mod-popup");
document.body.append(this._popupEl); document.body.append(this._popupEl);
this._popupEl.addEventListener("hass-more-info", async (ev) => {
const base = await hass_base_el();
console.log("More info", ev, base);
this._popupEl.closeDialog();
base.dispatchEvent(ev);
});
// const historyListener = async (ev) => { // const historyListener = async (ev) => {
// const popupState = ev.state?.browserModPopup; // const popupState = ev.state?.browserModPopup;
// if (popupState) { // if (popupState) {

View File

@@ -64,6 +64,7 @@ export const RequireInteractMixin = (SuperClass) => {
this._interactionResolve(); this._interactionResolve();
} }
}); });
video.pause();
} }
window.addEventListener( window.addEventListener(

View File

@@ -32,6 +32,7 @@ interface FullyKiosk {
declare global { declare global {
interface Window { interface Window {
browser_mod?: BrowserMod; browser_mod?: BrowserMod;
browser_mod_log?: any;
fully?: FullyKiosk; fully?: FullyKiosk;
hassConnection?: Promise<any>; hassConnection?: Promise<any>;
customCards?: [{}?]; customCards?: [{}?];

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "browser_mod", "name": "browser_mod",
"version": "2.0.0b2", "version": "2.0.0b4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,7 +1,7 @@
{ {
"name": "browser_mod", "name": "browser_mod",
"private": true, "private": true,
"version": "2.0.0b4", "version": "2.0.0",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",

25
test/automations.yaml Normal file
View File

@@ -0,0 +1,25 @@
- id: "1660669793583"
alias: Toggle bed light
description: ""
trigger:
- platform: time_pattern
seconds: /3
condition: []
action:
- type: toggle
device_id: 98861bdf58b3c79183c03be06da14f27
entity_id: light.bed_light
domain: light
mode: single
- alias: Popup when kitchen light togggled
trigger:
- platform: state
entity_id: light.kitchen_lights
action:
- service: browser_mod.popup
data:
title: automation
content:
type: markdown
content: "{%raw%}{{states('light.bed_light')}}{%endraw%}"

View File

@@ -1,5 +1,7 @@
default_config: default_config:
automation: !include test/automations.yaml
demo: demo:
http: http:
@@ -12,6 +14,8 @@ logger:
logs: logs:
custom_components.browser_mod: info custom_components.browser_mod: info
# debugpy:
# browser_mod: # browser_mod:
# devices: # devices:
# camdevice: # camdevice:

View File

@@ -32,6 +32,7 @@ views:
action: more-info action: more-info
- !include views/popup.yaml - !include views/popup.yaml
- !include views/frontend-backend.yaml
- title: Popup card - title: Popup card
popup_cards: popup_cards:

View File

@@ -0,0 +1,30 @@
title: frontend vs backend
cards:
- type: entities
entities:
- light.bed_light
- light.kitchen_lights
- type: button
name: fire-dom-event
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: fire-dom-event
content:
type: markdown
content: "{{states('light.bed_light')}}"
- type: button
name: call-service
tap_action:
action: call-service
service: browser_mod.popup
data:
title: call-service
content:
type: markdown
content: "{{states('light.bed_light')}}"