Compare commits
No commits in common. "1a3a7b2e953652d9c3b6dfa3f6d07cbe9fe11c00" and "c3f2ebd8d4cec458e03a152420a3622f7381c2f2" have entirely different histories.
1a3a7b2e95
...
c3f2ebd8d4
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "hass-browser_mod Dev",
|
|
||||||
"image": "thomasloven/hass-custom-devcontainer",
|
|
||||||
"postCreateCommand": "sudo -E container setup-dev && npm add",
|
|
||||||
"containerEnv": {
|
|
||||||
"DEVCONTAINER": "1"
|
|
||||||
},
|
|
||||||
"forwardPorts": [8123],
|
|
||||||
"mounts": [
|
|
||||||
"source=${localWorkspaceFolder},target=/config/www/workspace,type=bind",
|
|
||||||
"source=${localWorkspaceFolder}/test,target=/config/test,type=bind",
|
|
||||||
"source=${localWorkspaceFolder}/test/configuration.yaml,target=/config/configuration.yaml,type=bind",
|
|
||||||
"source=${localWorkspaceFolder}/custom_components,target=/config/custom_components,type=bind"
|
|
||||||
],
|
|
||||||
"runArgs": ["--env-file", "${localWorkspaceFolder}/test/.env"],
|
|
||||||
"extensions": [
|
|
||||||
"github.vscode-pull-request-github",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"spmeesseman.vscode-taskexplorer",
|
|
||||||
"ms-python.python"
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"files.eol": "\n",
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.formatOnPaste": false,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.formatOnType": true,
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"python.linting.pylintEnabled": false,
|
|
||||||
"python.linting.flake8Enabled": true,
|
|
||||||
"python.linting.enabled": true,
|
|
||||||
"python.formatting.provider": "black"
|
|
||||||
}
|
|
||||||
}
|
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,3 +1 @@
|
|||||||
custom_components/browser_mod/browser_mod.js binary
|
custom_components/browser_mod/browser_mod.js binary
|
||||||
custom_components/browser_mod/browser_mod_panel.js binary
|
|
||||||
package-lock.json binary
|
|
||||||
|
48
.github/ISSUE_TEMPLATE/bug-report.md
vendored
48
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: For reporting bugs or unexpected behavior
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
My Home Assistant version: 0.XX.X
|
|
||||||
|
|
||||||
**What I am doing:**
|
|
||||||
|
|
||||||
|
|
||||||
**What I expected to happen:**
|
|
||||||
|
|
||||||
|
|
||||||
**What happened instead:**
|
|
||||||
|
|
||||||
|
|
||||||
**Minimal** steps to reproduce:
|
|
||||||
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# The least amount of code or steps possible to reproduce my error
|
|
||||||
|
|
||||||
# Put your code/steps here
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
**Error messages from the browser console:**
|
|
||||||
|
|
||||||
```
|
|
||||||
// Select everything from the browser console and copy it
|
|
||||||
|
|
||||||
// Paste it here
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**By replacing the space in the checkboxes ([ ]) with an X below, I indicate that I:**
|
|
||||||
|
|
||||||
- [ ] Understand that this is a channel for reporting bugs, not a support forum (https://community.home-assistant.io/).
|
|
||||||
|
|
||||||
- [ ] Have made sure I am using the latest version of the plugin.
|
|
||||||
|
|
||||||
- [ ] Have followed the troubleshooting steps of the "Common Problems" section of https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins.
|
|
||||||
|
|
||||||
- [ ] Understand that leaving one or more boxes unticked or failure to follow the template above may increase the time required to handle my bug-report, or cause it to be closed without further action.
|
|
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: For suggesting new features
|
|
||||||
title: ''
|
|
||||||
labels: 'feature-request'
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
9
.github/stale.yml
vendored
9
.github/stale.yml
vendored
@ -1,9 +0,0 @@
|
|||||||
daysUntilStale: 60
|
|
||||||
daysUntilClose: 7
|
|
||||||
exemptLabels:
|
|
||||||
- pinned
|
|
||||||
- feature-request
|
|
||||||
staleLabel: stale
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
|
|
||||||
closeComment: false
|
|
14
.github/workflows/hassfest.yaml
vendored
14
.github/workflows/hassfest.yaml
vendored
@ -1,14 +0,0 @@
|
|||||||
name: Validate with hassfest
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate:
|
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
steps:
|
|
||||||
- uses: "actions/checkout@v2"
|
|
||||||
- uses: home-assistant/actions/hassfest@master
|
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
.vscode
|
|
||||||
.env
|
|
||||||
custom_components/hacs/
|
|
475
README.md
475
README.md
@ -1,108 +1,429 @@
|
|||||||
# browser_mod 2.0
|
browser\_mod
|
||||||
|
============
|
||||||
|
|
||||||
[](https://github.com/custom-components/hacs)
|
[](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?
|
A Home Assistant integration to turn your browser into a controllable entity - and also an audio player and a security camera (WIP).
|
||||||
|
|
||||||
And what if you could use it as an extra security camera?
|
## Example uses
|
||||||
|
|
||||||
Or what if you could use it to play music and videos from your Home Assistant media library?
|
- Make the camera feed from your front door pop up on the tablet in your kitchen when someone rings the doorbell.
|
||||||
|
- Have a message pop up on every screen in the house when it's bedtime.
|
||||||
|
- Make the browser on your workstation switch to a specific tab when the kitchen light is on after midnight
|
||||||
|
- Play a TTS message on your work computer when the traffic sensor tells you it's time to go home.
|
||||||
|
- Display a full screen clock on your screen if no one's touched it for five minutes
|
||||||
|
|
||||||
What if you could permanently hide that sidebar from your kids and lock them into a single dashboard?
|
For more usage examples, see the [cookbook](https://github.com/thomasloven/hass-browser_mod/wiki/Cookbook).
|
||||||
|
|
||||||
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**
|
- Copy the contents of `custom_components/browser_mod/` to `<your config dir>/custom_components/browser_mod/`.
|
||||||
|
|
||||||
- Either
|
- Add the following to your `configuration.yaml`:
|
||||||
|
|
||||||
- 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/`.
|
|
||||||
|
|
||||||
- Restart Home Assistant
|
|
||||||
|
|
||||||
- Add the "Browser Mod" integration in Settings -> Devices & Services -> Add Integration or click this button: [](https://my.home-assistant.io/redirect/config_flow_start/?domain=browser_mod)
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
\
|
|
||||||
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
### See [Configuration Panel](documentation/configuration-panel.md) for more info
|
|
||||||
\
|
|
||||||
|
|
||||||
|
|
||||||
# Browser Mod Services
|
|
||||||
|
|
||||||
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
|
|
||||||
\
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Popup card
|
|
||||||
|
|
||||||
A popup card can be used to replace the more-info dialog of an entity with something of your choosing.
|
|
||||||
|
|
||||||
To use it, add a "Custom: Popup card" to a dashboard view via the GUI, pick the entity you want to override, configure the card and set up the popup like for the [`browser_mod.popup` service](documentation/services.md).
|
|
||||||
|
|
||||||
The card will be visible only while you're in Edit mode.
|
|
||||||
|
|
||||||
As long as the popup card is (would be) visible, i.e. you stay on the same view;
|
|
||||||
whenever the more-info dialog for the entitiy you selected would be opened, the popup card will be shown instead.
|
|
||||||
|
|
||||||
Yaml configuration:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
type: custom:popup-card
|
browser_mod:
|
||||||
entity: <entity id>
|
|
||||||
card:
|
|
||||||
type: ...etc...
|
|
||||||
[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.
|
- Restart Home Assistant
|
||||||
|
|
||||||
## Browser Player
|
> Note: If you want to use browser_mod with Home Assistant Cast, you need to also add:
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> resources:
|
||||||
|
> - url: /browser_mod.js
|
||||||
|
> type: module
|
||||||
|
> ```
|
||||||
|
> to your `ui_lovelace.yaml`.
|
||||||
|
> Don't worry about where to put browser_mod.js, the integration will handle that automatically, and please note that it's **not** `/local/browser_mod.js`.
|
||||||
|
|
||||||
Browser player is a card that allows you to controll the volume and playback on the current Browsers media player.
|
|
||||||
|
|
||||||
Add it to a dashboard via the GUI or through yaml:
|
# Usage
|
||||||
|
|
||||||
|
## Devices
|
||||||
|
The most important concept of `browser_mod` is the *device*.
|
||||||
|
|
||||||
|
A *device* is a machine-browser combination identified by a unique `deviceID`. The `deviceID` is randomly generated and may look like `ded3b4dc-abedd098`.
|
||||||
|
|
||||||
|
- Chrome on your desktop and Chrome on your laptop are two different *devices*.
|
||||||
|
- Chrome on your laptop and Safari on your laptop are two different *devices*.
|
||||||
|
- Two tabs in Firefox on the same computer is one *device*.
|
||||||
|
- Two windows in Edge on the same computer is one *device*.
|
||||||
|
|
||||||
|
In the two latter cases, the last loaded tab/window will be the *active* one.
|
||||||
|
|
||||||
|
Note: Incognito mode will generate a new `deviceID` and thus a new *device* every time it's started.
|
||||||
|
|
||||||
|
### Aliases
|
||||||
|
Since the deviceID can be a bit hard to remember for devices you use often, you can specify an alias in `configuration.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
browser_mod:
|
||||||
|
devices:
|
||||||
|
99980b13-dabc9563:
|
||||||
|
name: arrakis
|
||||||
|
d2fc860c-16379d23:
|
||||||
|
name: dashboard
|
||||||
|
```
|
||||||
|
This binds the *aliases* `arrakis` to `99980b13-dabc9563` and `dashboard` to `d2fc860c-16379d23`.
|
||||||
|
|
||||||
|
Note: Aliases must be unique.
|
||||||
|
|
||||||
|
### Prefix
|
||||||
|
You can add a custom prefix to all entity ids in `configuration.yaml`:
|
||||||
|
|
||||||
|
E.g. to give entities default names like `media_player.browser_99980b13_dabc9563` add:
|
||||||
|
```yaml
|
||||||
|
browser_mod:
|
||||||
|
prefix: "browser_"
|
||||||
|
```
|
||||||
|
This does not apply to devices with an alias.
|
||||||
|
|
||||||
|
### Disabling entities
|
||||||
|
`browser_mod` creates a number of entities, which is explained below. In some cases, you may not want to do that. If so, add a list of entity types you do *not* want to add to a `disable` section, either for each device, or globally to ignore for all unknown devices:
|
||||||
|
|
||||||
|
E.g. to disable the `light` and `media_player` for the device aliased to `arrakis`, AND disable *all* entities for all devices which *don't* have an alias:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
browser_mod:
|
||||||
|
devices:
|
||||||
|
99980b13-dabc9563:
|
||||||
|
name: arrakis
|
||||||
|
disable:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
disable:
|
||||||
|
- all
|
||||||
|
````
|
||||||
|
|
||||||
|
|
||||||
|
## Entities
|
||||||
|
Once `browser_mod` is installed, loading up your Home Assistant frontend on a new *device* will create three to five new devices.
|
||||||
|
|
||||||
|
- `sensor.<device>`
|
||||||
|
- `media_player.<device>`
|
||||||
|
- `light.<device>`
|
||||||
|
- If you've enabled it: `camera.<device>`
|
||||||
|
- If you're using Fully Kiosk Browser: `binary_sensor.<device>`
|
||||||
|
|
||||||
|
`<device>` here will be the `deviceID` of the *device* but with the dash (`-`) replaced by an underscore (`_`). If you've defined an alias, it will be that instead.
|
||||||
|
|
||||||
|
E.g:
|
||||||
|
Connecting your phone with `deviceID: ded3b4dc-abedd098` will create the entities `sensor.ded3b4dc_abedd098`, `media_player.ded3b4dc_abedd098` and `light.ded3b4dc_abedd098`.
|
||||||
|
Connecting with the computer named `Arrakis` above with `deviceID: 99980b13-dabc9563` will create the entities `sensor.arrakis`, `media_player.arrakis` and `light.arrakis`.
|
||||||
|
|
||||||
|
### sensor
|
||||||
|
|
||||||
|
The `sensor` will display the number of connected views (tabs/windows) of the device. Note that using multiple view isn't really recommended, and any action targeting a device will happen in the last loaded view.
|
||||||
|
|
||||||
|
The sensor also has the following attributes:
|
||||||
|
|
||||||
|
| attribute | content |
|
||||||
|
| --- | --- |
|
||||||
|
| `type` | `browser_mod` |
|
||||||
|
| `last_seen` | The time when the *device* was last seen |
|
||||||
|
| `deviceID` | The deviceID of the *device*. |
|
||||||
|
| `path` | The currently displayed path on the *device*. |
|
||||||
|
| `visibility` | Whether the frontend is currently visible on the *device*. |
|
||||||
|
| `userAgent` | The User Agent of the associated browser. |
|
||||||
|
| `currentUser` | The user currently logged in on the *device*. |
|
||||||
|
| `fullyKiosk` | True if the *device* is a Fully Kiosk browser. Undefined otherwise. |
|
||||||
|
| `width` | The current width of the browser window in pixels. |
|
||||||
|
| `height` | The current height of the browser window in pixels. |
|
||||||
|
|
||||||
|
### media\_player
|
||||||
|
|
||||||
|
The `media_player` can be used to play sounds on the *device*.
|
||||||
|
|
||||||
|
**NOTE: Because Apple is Apple; on iOS you need to touch the screen once after loading the frontend before any playback will work.**
|
||||||
|
|
||||||
|
### light
|
||||||
|
|
||||||
|
The `light` can be used to blackout the screen.
|
||||||
|
For Fully Kiosk Browser, the screen will actually turn off.
|
||||||
|
For other browsers, the interface will just be covered with black (the screen is still on, will have a visible glow in the dark, and you won't save any battery).
|
||||||
|
|
||||||
|
### camera (EXPERIMENTAL)
|
||||||
|
|
||||||
|
For security and UX reasons, the camera must be enabled manually on a device by device basis.
|
||||||
|
|
||||||
|
Enabling the camera is done by adding `camera: true` to the devices configuration in `configuration.yaml`:
|
||||||
|
```yaml
|
||||||
|
browser_mod:
|
||||||
|
devices:
|
||||||
|
99980b13-dabc9563:
|
||||||
|
name: arrakis
|
||||||
|
camera: true
|
||||||
|
d2fc860c-16379d23:
|
||||||
|
name: dashboard
|
||||||
|
```
|
||||||
|
After restarting Home Assistant (and [clearing cache](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins#clearing-cache)), the next time you load your interface your browser will ask you if you want Home Assistant to be able to access your camera. Some browsers (e.g. mobile Safari) will ask every time you make a hard refresh.
|
||||||
|
|
||||||
|
Be aware that keeping the camera on may make your device run hot and drain your battery.
|
||||||
|
|
||||||
|
### binary\_sensor
|
||||||
|
|
||||||
|
The `binary_sensor` will only be available for Fully Kiosk Browser PRO *devices*.
|
||||||
|
It's state will be the state of the camera motion detector of the *device* (5 second cooldown).
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
`browser_mod` registers a number of services.
|
||||||
|
|
||||||
|
All service calls have one parameter in common; `deviceID` which is a list of *devices* to execute the comand on. If `deviceID` is omitted, the command will be executed on **all** currenctly connected *devices*. `deviceID` may also contain aliases.
|
||||||
|
|
||||||
|
If a service is called from the frontend (e.g. by using the `call-service` tap action), a value of `this` in the `deviceID` list will be replaced with the ID of the *device* the call was made from.
|
||||||
|
Alternatively, `deviceID: this` will also work.
|
||||||
|
|
||||||
|
All examples below are given in the syntax used for calling them from lovelace via e.g. an entity-button card with `tap_action:` set to `call-service`. If you call the service from a script or an automation, the syntax will be slightly different.
|
||||||
|
|
||||||
|
### debug
|
||||||
|
|
||||||
|
```
|
||||||
|
service: browser_mod.debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Display a popup with the deviceID *and* a javascript alert with the deviceID on all connected *devices*.
|
||||||
|
|
||||||
|
### set_theme
|
||||||
|
|
||||||
|
```
|
||||||
|
service: browser_mod.set_theme
|
||||||
|
service_data:
|
||||||
|
theme: clear_light
|
||||||
|
```
|
||||||
|
|
||||||
|
will set the current theme to `clear_light` on all devices.
|
||||||
|
|
||||||
|
### navigate
|
||||||
|
```
|
||||||
|
service: browser_mod.navigate
|
||||||
|
service_data:
|
||||||
|
navigation_path: /lovelace/1
|
||||||
|
deviceID:
|
||||||
|
- ded3b4dc-abedd098
|
||||||
|
```
|
||||||
|
|
||||||
|
will open your second lovelace view on just the *device* `ded3b4dc-abedd098`.
|
||||||
|
|
||||||
|
Note: `navigation_path` does not have to be a lovelace path. All paths in Home Assistant works. (E.g. `/states`, `/dev-info`, `/map`)
|
||||||
|
|
||||||
|
### more_info
|
||||||
|
```
|
||||||
|
service: browser_mod.more_info
|
||||||
|
service_data:
|
||||||
|
entity_id: camera.front_door
|
||||||
|
deviceID:
|
||||||
|
- ded3b4dc-abedd098
|
||||||
|
- dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
will show the more-info dialog of `camera.front_door` on the *devices* `ded3b4dc-abedd098` and `dashboard`.
|
||||||
|
|
||||||
|
The optional parameter `large: true` will make the popup wider.
|
||||||
|
|
||||||
|
### toast
|
||||||
|
```
|
||||||
|
service: browser_mod.toast
|
||||||
|
service_data:
|
||||||
|
message: Short message
|
||||||
|
```
|
||||||
|
|
||||||
|
Display a toast notification on all devices.
|
||||||
|
The optional parameter `duration:` determines the time (in ms) that the toast is shown. Set to 0 for persistent display. Default is 3000.
|
||||||
|
|
||||||
|
### popup
|
||||||
|
```
|
||||||
|
service: browser_mod.popup
|
||||||
|
service_data:
|
||||||
|
title: Popup example
|
||||||
|
card:
|
||||||
|
type: entities
|
||||||
|
entities:
|
||||||
|
- light.bed_light
|
||||||
|
- light.kitchen_lights
|
||||||
|
- light.ceiling_lights
|
||||||
|
deviceID:
|
||||||
|
- this
|
||||||
|
- dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
will display the specified `entities` card as a popup on the current device and on `dashboard`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The optional parameter `large: true` will make the popup wider.
|
||||||
|
The optional parameter `style:` will apply CSS style options to the popup.
|
||||||
|
The optional parameter `auto_close: true` will make the popup close automatically when the mouse is moved or a key is pressed on the keyboard. This also removes the header bar.
|
||||||
|
The optional parameter `time:` (only useable if `auto_close: true` is also set) will turn the popup into a "screensaver". See the `blackout` command below.
|
||||||
|
|
||||||
|
Ex:
|
||||||
|
```yaml
|
||||||
|
style:
|
||||||
|
border-radius: 20px
|
||||||
|
--ha-card-border-radius: 20px
|
||||||
|
--ha-card-background: red
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Sometimes this doesn't work if the *device* is not currently displaying a lovelace path. I'm looking into that...
|
||||||
|
|
||||||
|
### close_popup
|
||||||
|
```
|
||||||
|
service: browser_mod.close_popup
|
||||||
|
```
|
||||||
|
|
||||||
|
will close all more-info dialogs and popups that are open on all connected *devices*.
|
||||||
|
|
||||||
|
### blackout
|
||||||
|
```
|
||||||
|
service: browser_mod.blackout
|
||||||
|
service_data:
|
||||||
|
deviceID: this
|
||||||
|
```
|
||||||
|
|
||||||
|
Will cover the entire window (or screen if in full screen mode) with black on the current device.
|
||||||
|
Moving the mouse, touching the screen or pressing any key will restore the view.
|
||||||
|
|
||||||
|
The optional parameter `time:` will make the blackout 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`.
|
||||||
|
|
||||||
|
Note: This will *not* turn off your screen backlight. Most screens will still emit light in a dark room.
|
||||||
|
|
||||||
|
### no_blackout
|
||||||
|
```
|
||||||
|
service: browser_mod.no_blackout
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove a blackout.
|
||||||
|
The optional parameter `brightness` will set the screen brightness of a device running Fully Kiosk Browser to a value between 0 and 255.
|
||||||
|
|
||||||
|
### lovelace_reload
|
||||||
|
```
|
||||||
|
service: browser_mod.lovelace_reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Refreshes the lovelace config. Same as clicking "Refresh" in the top right menu in lovelace.
|
||||||
|
|
||||||
|
|
||||||
|
## `browser-player` card
|
||||||
|
|
||||||
|
To control the playback in the current *device*, `browser_mod` includes a custom lovelace card. Just add
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
type: custom:browser-player
|
type: custom:browser-player
|
||||||
```
|
```
|
||||||
|
|
||||||
|
anywhere in your lovelace configuration.
|
||||||
|
|
||||||
|
The player card also displays the `entityID`. Click it to select, so you can copy it.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Fully Kiosk Browser
|
||||||
|
If you are using a device running [Fully Kiosk Browser](https://www.ozerov.de/fully-kiosk-browser/) (PLUS version only) you will have access to a few more functions.
|
||||||
|
|
||||||
|
For this to work you need to activate `Settings->Advanced Web Settings->Javascript Interface (PLUS)` and `Settings->Motion Detection (PLUS)->Enable Visual Motion Detection`.
|
||||||
|
|
||||||
|
First of all the commands `blackout` and `no-blackout` will control the devices screen directly.
|
||||||
|
`no-blackout` also has an optional parameter `brightness` that can set the screen brightness between 0 and 255.
|
||||||
|
|
||||||
|
Second, there are a few more attributes available
|
||||||
|
|
||||||
|
| attribute | content |
|
||||||
|
| --- | --- |
|
||||||
|
| `fullyKiosk` | True. |
|
||||||
|
| `brightness` | The current screen brightness. |
|
||||||
|
| `battery` | The current charge percentage of the devices battery. |
|
||||||
|
| `charging` | Whether the battery is currently charging. |
|
||||||
|
| `motion` | Whether the devices camera has detected any motion in the last five seconds. |
|
||||||
|
|
||||||
|
# Replacing more-info dialogs
|
||||||
|
With browser_mod, you can replace any more-info dialog with any lovelace card you choose yourself. This can be done either per lovelace view, or globally (even outside of lovelace).
|
||||||
|
|
||||||
|
The replacement is included in your lovelace or lovelace view configuration, and the syntax is exactly like the `popup` service, except you can't use `auto_close` or `time`.
|
||||||
|
|
||||||
|
Ex:
|
||||||
|
```yaml
|
||||||
|
views:
|
||||||
|
- title: Home view
|
||||||
|
icon: mdi:house
|
||||||
|
popup_cards:
|
||||||
|
light.ceiling_light:
|
||||||
|
title: My popup
|
||||||
|
card:
|
||||||
|
type: entities
|
||||||
|
entities:
|
||||||
|
- light.ceiling_light_bulb1
|
||||||
|
- light.ceiling_light_bulb2
|
||||||
|
- light.ceiling_light_bulb3
|
||||||
|
- light.ceiling_light_bulb4
|
||||||
|
```
|
||||||
|
|
||||||
|
This would show an entities card with four bulbs any time the more-info dialog for `light.ceiling_light` would normally be shown when you're on the Home view in lovelace.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
title: My home
|
||||||
|
resources:
|
||||||
|
- url: /local/card-mod.js
|
||||||
|
type: module
|
||||||
|
popup_cards:
|
||||||
|
sensor.sensor1:
|
||||||
|
title: First sensor
|
||||||
|
card:
|
||||||
|
type: gauge
|
||||||
|
entity: sensor.sensor1
|
||||||
|
sensor.sensor2:
|
||||||
|
title: Second sensor
|
||||||
|
card:
|
||||||
|
type: gauge
|
||||||
|
entity: sensor.sensor2
|
||||||
|
```
|
||||||
|
This would replace the more-info dialogs of `sensor.sensor1` and `sensor.sensor2` anywhere in your interface. Even outside of lovelace - be careful about that.
|
||||||
|
|
||||||
|
# Support
|
||||||
|
|
||||||
|
[Home Assistant community forum thread](https://community.home-assistant.io/t/browser-mod-turn-your-browser-into-a-controllable-device-and-a-media-player/123806)
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
### **Why doesn't ANYTHING that used to work with Browser Mod 1.0 work with Browser Mod 2.0?**
|
### Where can I find my deviceID?
|
||||||
|
|
||||||
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.
|
The easiest way is to go to `developer-tools/service` and call the `browser_mod.debug` service.
|
||||||
|
|
||||||
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.
|
But you can also find the device id on the `browser-player` card, if you added one to your lovelace config.
|
||||||
|
|
||||||
### **Why does my Browser ID keep changing?**
|
An alternative way is to type `localStorage["lovelace-player-device-id"]` into your browsers console.
|
||||||
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...
|
|
||||||
|
### Does this replace lovelace-player and lovelace-browser-commander
|
||||||
|
Yes.
|
||||||
|
|
||||||
|
Some improvements
|
||||||
|
|
||||||
|
- With the backend support `browser_mod` does the same things as both of those, but better.
|
||||||
|
- Since `browser_mod` uses a service for executing commands rather than events, the commands can be easily triggered by any lovelace element which has a `tap_action` setting.
|
||||||
|
This actually means it pretty much replaces `popup-card` as well.
|
||||||
|
- `browser_mod` uses websockets to get immediate feedback from the *device* to the backend and much better tracking of disconnects.
|
||||||
|
- *Aliases*. 'nuff said.
|
||||||
|
- `browser_mod` works outside of `/lovelace`.
|
||||||
|
- This works even if the currently logged in user is not in the admin group.
|
||||||
|
|
||||||
|
### Does this replace lovelace-fullykiosk
|
||||||
|
Yes. You need the paid version, btw.
|
||||||
|
|
||||||
|
### Can the deviceID be used to track me across the internet
|
||||||
|
|
||||||
|
No\*. The device is stored in your browsers localStorage - a data store which is sandboxed only to Home Assistant. That means only Home Assistant can access it. Furthermore, different Home Assistant installations cannot acces each others localStorage.
|
||||||
|
|
||||||
|
Some of [my lovelace plugins](https://github.com/thomasloven/hass-config/wiki/My-Lovelace-Plugins) use the device to do different things for different *devices*.
|
||||||
|
|
||||||
|
**\*: There is one exception. If you are using [Fully Kiosk Browser](https://www.ozerov.de/fully-kiosk-browser/), the deviceID is taken from the browser instead of being randomly generated. This deviceID will be the same for each website that asks for it.**
|
||||||
|
|
||||||
|
### How do I run commands from /dev-service?
|
||||||
|
|
||||||
|
`/dev-service` requires json-formatted service data. There's an explanation on the differences between yaml and json [here](http://thomasloven.com/blog/2018/08/YAML-For-Nonprogrammers/).
|
||||||
|
|
||||||
|
### How do I run commands from a script/automation?
|
||||||
|
|
||||||
|
Basically, just replace `service_data` with `data` or `data_template`, whichever fits your needs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<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>
|
||||||
|
@ -1,37 +1,41 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .store import BrowserModStore
|
from .mod_view import setup_view
|
||||||
from .mod_view import async_setup_view
|
from .connection import setup_connection
|
||||||
from .connection import async_setup_connection
|
from .service import setup_service
|
||||||
from .const import DOMAIN, DATA_BROWSERS, DATA_ADDERS, DATA_STORE
|
from .const import DOMAIN, DATA_DEVICES, DATA_ALIASES, DATA_ADDERS, CONFIG_DEVICES, DATA_CONFIG
|
||||||
from .service import async_setup_services
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
|
|
||||||
store = BrowserModStore(hass)
|
setup_view(hass)
|
||||||
await store.load()
|
|
||||||
|
aliases = {}
|
||||||
|
for d in config[DOMAIN].get(CONFIG_DEVICES, {}):
|
||||||
|
name = config[DOMAIN][CONFIG_DEVICES][d].get("name", None)
|
||||||
|
if name:
|
||||||
|
aliases[name] = d.replace('_','-')
|
||||||
|
|
||||||
hass.data[DOMAIN] = {
|
hass.data[DOMAIN] = {
|
||||||
DATA_BROWSERS: {},
|
DATA_DEVICES: {},
|
||||||
|
DATA_ALIASES: aliases,
|
||||||
DATA_ADDERS: {},
|
DATA_ADDERS: {},
|
||||||
DATA_STORE: store,
|
DATA_CONFIG: config[DOMAIN],
|
||||||
}
|
}
|
||||||
|
|
||||||
return True
|
await hass.helpers.discovery.async_load_platform("media_player", DOMAIN, {}, config)
|
||||||
|
await hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config)
|
||||||
|
await hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config)
|
||||||
|
await hass.helpers.discovery.async_load_platform("light", DOMAIN, {}, config)
|
||||||
|
await hass.helpers.discovery.async_load_platform("camera", DOMAIN, {}, config)
|
||||||
|
|
||||||
|
await setup_connection(hass, config)
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry):
|
setup_service(hass)
|
||||||
|
|
||||||
for domain in ["sensor", "binary_sensor", "light", "media_player", "camera"]:
|
|
||||||
hass.async_create_task(
|
|
||||||
hass.config_entries.async_forward_entry_setup(config_entry, domain)
|
|
||||||
)
|
|
||||||
|
|
||||||
await async_setup_connection(hass)
|
|
||||||
await async_setup_view(hass)
|
|
||||||
await async_setup_services(hass)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,47 +1,50 @@
|
|||||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_ADDERS
|
from homeassistant.const import STATE_UNAVAILABLE, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, STATE_ON, STATE_OFF
|
||||||
from .entities import BrowserModEntity
|
from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION
|
||||||
|
|
||||||
|
from .helpers import setup_platform, BrowserModEntity
|
||||||
|
|
||||||
async def async_setup_platform(
|
PLATFORM = 'binary_sensor'
|
||||||
hass, config_entry, async_add_entities, discoveryInfo=None
|
|
||||||
):
|
|
||||||
hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor)
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
class BrowserModSensor(BrowserModEntity):
|
||||||
await async_setup_platform(hass, {}, async_add_entities)
|
domain = PLATFORM
|
||||||
|
|
||||||
|
def __init__(self, hass, connection, deviceID, alias=None):
|
||||||
|
super().__init__(hass, connection, deviceID, alias)
|
||||||
|
self.last_seen = None
|
||||||
|
|
||||||
class BrowserBinarySensor(BrowserModEntity, BinarySensorEntity):
|
def updated(self):
|
||||||
def __init__(self, coordinator, browserID, parameter, name, icon=None):
|
self.last_seen = datetime.now()
|
||||||
BrowserModEntity.__init__(self, coordinator, browserID, name, icon)
|
self.schedule_update_ha_state()
|
||||||
BinarySensorEntity.__init__(self)
|
|
||||||
self.parameter = parameter
|
@property
|
||||||
|
def state(self):
|
||||||
|
if not self.connection.connection:
|
||||||
|
return STATE_UNAVAILABLE
|
||||||
|
if self.data.get('motion', False):
|
||||||
|
return STATE_ON
|
||||||
|
return STATE_OFF
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
return self._data.get("browser", {}).get(self.parameter, None)
|
return not self.data.get('motion', False)
|
||||||
|
|
||||||
|
|
||||||
class ActivityBinarySensor(BrowserModEntity, BinarySensorEntity):
|
|
||||||
def __init__(self, coordinator, browserID):
|
|
||||||
BrowserModEntity.__init__(self, coordinator, browserID, None)
|
|
||||||
BinarySensorEntity.__init__(self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
return f"{self.browserID}-activity"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entity_registry_visible_default(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
return "motion"
|
return DEVICE_CLASS_MOTION
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def device_state_attributes(self):
|
||||||
return self._data.get("activity", False)
|
return {
|
||||||
|
"type": "browser_mod",
|
||||||
|
"last_seen": self.last_seen,
|
||||||
|
ATTR_BATTERY_LEVEL: self.data.get('battery', None),
|
||||||
|
ATTR_BATTERY_CHARGING: self.data.get('charging', None),
|
||||||
|
**self.data
|
||||||
|
}
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.components.websocket_api import event_message
|
|
||||||
from homeassistant.helpers import device_registry, entity_registry
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
from homeassistant.core import callback
|
|
||||||
|
|
||||||
from .const import DATA_BROWSERS, DOMAIN, DATA_ADDERS
|
|
||||||
from .sensor import BrowserSensor
|
|
||||||
from .light import BrowserModLight
|
|
||||||
from .binary_sensor import BrowserBinarySensor, ActivityBinarySensor
|
|
||||||
from .media_player import BrowserModPlayer
|
|
||||||
from .camera import BrowserModCamera
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Coordinator(DataUpdateCoordinator):
|
|
||||||
def __init__(self, hass, browserID):
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name="Browser Mod Coordinator",
|
|
||||||
)
|
|
||||||
self.browserID = browserID
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserModBrowser:
|
|
||||||
"""A Browser_mod browser.
|
|
||||||
Handles the Home Assistant device corresponding to a registered Browser.
|
|
||||||
Creates and updates entities based on available data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, hass, browserID):
|
|
||||||
self.browserID = browserID
|
|
||||||
self.coordinator = Coordinator(hass, browserID)
|
|
||||||
self.entities = {}
|
|
||||||
self.data = {}
|
|
||||||
self.settings = {}
|
|
||||||
self._connections = []
|
|
||||||
|
|
||||||
self.update_entities(hass)
|
|
||||||
|
|
||||||
def update(self, hass, newData):
|
|
||||||
"""Update state of all related entities."""
|
|
||||||
self.data.update(newData)
|
|
||||||
self.update_entities(hass)
|
|
||||||
self.coordinator.async_set_updated_data(self.data)
|
|
||||||
|
|
||||||
def update_settings(self, hass, settings):
|
|
||||||
"""Update Browser settings and entities if needed."""
|
|
||||||
self.settings = settings
|
|
||||||
self.update_entities(hass)
|
|
||||||
|
|
||||||
def update_entities(self, hass):
|
|
||||||
"""Create all entities associated with the browser."""
|
|
||||||
|
|
||||||
coordinator = self.coordinator
|
|
||||||
browserID = self.browserID
|
|
||||||
|
|
||||||
def _assert_browser_sensor(type, name, *properties, **kwarg):
|
|
||||||
"""Create a browser state sensor if it does not already exist"""
|
|
||||||
if name in self.entities:
|
|
||||||
return
|
|
||||||
adder = hass.data[DOMAIN][DATA_ADDERS][type]
|
|
||||||
cls = {"sensor": BrowserSensor, "binary_sensor": BrowserBinarySensor}[type]
|
|
||||||
new = cls(coordinator, browserID, name, *properties, **kwarg)
|
|
||||||
adder([new])
|
|
||||||
self.entities[name] = new
|
|
||||||
|
|
||||||
_assert_browser_sensor("sensor", "path", "Browser path", icon="mdi:web")
|
|
||||||
_assert_browser_sensor("sensor", "visibility", "Browser visibility")
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"sensor", "userAgent", "Browser userAgent", icon="mdi:account-details"
|
|
||||||
)
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"sensor", "currentUser", "Browser user", icon="mdi:account"
|
|
||||||
)
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"sensor", "width", "Browser width", "px", icon="mdi:arrow-left-right"
|
|
||||||
)
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"sensor", "height", "Browser height", "px", icon="mdi:arrow-up-down"
|
|
||||||
)
|
|
||||||
# Don't create battery sensor unless battery level is reported
|
|
||||||
if self.data.get("browser", {}).get("battery_level", None) is not None:
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"sensor", "battery_level", "Browser battery", "%", "battery"
|
|
||||||
)
|
|
||||||
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"binary_sensor",
|
|
||||||
"darkMode",
|
|
||||||
"Browser dark mode",
|
|
||||||
icon="mdi:theme-light-dark",
|
|
||||||
)
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"binary_sensor", "fullyKiosk", "Browser FullyKiosk", icon="mdi:alpha-f"
|
|
||||||
)
|
|
||||||
# Don't create a charging sensor unless charging state is reported
|
|
||||||
if self.data.get("browser", {}).get("charging", None) is not None:
|
|
||||||
_assert_browser_sensor(
|
|
||||||
"binary_sensor", "charging", "Browser charging", icon="mdi:power-plug"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "activity" not in self.entities:
|
|
||||||
adder = hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"]
|
|
||||||
new = ActivityBinarySensor(coordinator, browserID)
|
|
||||||
adder([new])
|
|
||||||
self.entities["activity"] = new
|
|
||||||
|
|
||||||
if "screen" not in self.entities:
|
|
||||||
adder = hass.data[DOMAIN][DATA_ADDERS]["light"]
|
|
||||||
new = BrowserModLight(coordinator, browserID, self)
|
|
||||||
adder([new])
|
|
||||||
self.entities["screen"] = new
|
|
||||||
|
|
||||||
if "player" not in self.entities:
|
|
||||||
adder = hass.data[DOMAIN][DATA_ADDERS]["media_player"]
|
|
||||||
new = BrowserModPlayer(coordinator, browserID, self)
|
|
||||||
adder([new])
|
|
||||||
self.entities["player"] = new
|
|
||||||
|
|
||||||
if "camera" not in self.entities and self.settings.get("camera"):
|
|
||||||
adder = hass.data[DOMAIN][DATA_ADDERS]["camera"]
|
|
||||||
new = BrowserModCamera(coordinator, browserID)
|
|
||||||
adder([new])
|
|
||||||
self.entities["camera"] = new
|
|
||||||
if "camera" in self.entities and not self.settings.get("camera"):
|
|
||||||
er = entity_registry.async_get(hass)
|
|
||||||
er.async_remove(self.entities["camera"].entity_id)
|
|
||||||
del self.entities["camera"]
|
|
||||||
|
|
||||||
hass.create_task(
|
|
||||||
self.send(
|
|
||||||
None, browserEntities={k: v.entity_id for k, v in self.entities.items()}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
async def send(self, command, **kwargs):
|
|
||||||
"""Send a command to this browser."""
|
|
||||||
if self.connection is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for (connection, cid) in self.connection:
|
|
||||||
connection.send_message(
|
|
||||||
event_message(
|
|
||||||
cid,
|
|
||||||
{
|
|
||||||
"command": command,
|
|
||||||
**kwargs,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, hass):
|
|
||||||
"""Delete the device and associated entities."""
|
|
||||||
dr = device_registry.async_get(hass)
|
|
||||||
er = entity_registry.async_get(hass)
|
|
||||||
|
|
||||||
for e in self.entities.values():
|
|
||||||
er.async_remove(e.entity_id)
|
|
||||||
|
|
||||||
self.entities = {}
|
|
||||||
|
|
||||||
device = dr.async_get_device({(DOMAIN, self.browserID)})
|
|
||||||
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
|
|
||||||
def connection(self):
|
|
||||||
"""The current websocket connections for this Browser."""
|
|
||||||
return self._connections
|
|
||||||
|
|
||||||
def open_connection(self, connection, cid):
|
|
||||||
"""Add a websocket connection."""
|
|
||||||
self._connections.append((connection, cid))
|
|
||||||
|
|
||||||
def close_connection(self, connection):
|
|
||||||
"""Close a websocket connection."""
|
|
||||||
self._connections = list(
|
|
||||||
filter(lambda v: v[0] != connection, self._connections)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def getBrowser(hass, browserID, *, create=True):
|
|
||||||
"""Get or create browser by browserID."""
|
|
||||||
browsers = hass.data[DOMAIN][DATA_BROWSERS]
|
|
||||||
if browserID in browsers:
|
|
||||||
return browsers[browserID]
|
|
||||||
|
|
||||||
if not create:
|
|
||||||
return None
|
|
||||||
|
|
||||||
browsers[browserID] = BrowserModBrowser(hass, browserID)
|
|
||||||
return browsers[browserID]
|
|
||||||
|
|
||||||
|
|
||||||
def deleteBrowser(hass, browserID):
|
|
||||||
"""Delete a browser by BrowserID."""
|
|
||||||
browsers = hass.data[DOMAIN][DATA_BROWSERS]
|
|
||||||
if browserID in browsers:
|
|
||||||
browsers[browserID].delete(hass)
|
|
||||||
del browsers[browserID]
|
|
||||||
|
|
||||||
|
|
||||||
def getBrowserByConnection(hass, connection):
|
|
||||||
"""Get the browser that has a given connection open."""
|
|
||||||
browsers = hass.data[DOMAIN][DATA_BROWSERS]
|
|
||||||
|
|
||||||
for k, v in browsers.items():
|
|
||||||
if any([c[0] == connection for c in v.connection]):
|
|
||||||
return v
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,39 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF, STATE_IDLE
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
|
|
||||||
from .entities import BrowserModEntity
|
from .helpers import setup_platform, BrowserModEntity
|
||||||
from .const import DOMAIN, DATA_ADDERS
|
|
||||||
|
|
||||||
import logging
|
PLATFORM = 'camera'
|
||||||
|
|
||||||
LOGGER = logging.Logger(__name__)
|
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModCamera)
|
||||||
|
|
||||||
|
class BrowserModCamera(Camera, BrowserModEntity):
|
||||||
|
domain = PLATFORM
|
||||||
|
|
||||||
async def async_setup_platform(
|
def __init__(self, hass, connection, deviceID, alias=None):
|
||||||
hass, config_entry, async_add_entities, discoveryInfo=None
|
|
||||||
):
|
|
||||||
hass.data[DOMAIN][DATA_ADDERS]["camera"] = async_add_entities
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
||||||
await async_setup_platform(hass, {}, async_add_entities)
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserModCamera(BrowserModEntity, Camera):
|
|
||||||
def __init__(self, coordinator, browserID):
|
|
||||||
BrowserModEntity.__init__(self, coordinator, browserID, None)
|
|
||||||
Camera.__init__(self)
|
Camera.__init__(self)
|
||||||
|
BrowserModEntity.__init__(self, hass, connection, deviceID, alias)
|
||||||
|
self.last_seen = None
|
||||||
|
|
||||||
|
def updated(self):
|
||||||
|
self.last_seen = datetime.now()
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def camera_image(self):
|
||||||
|
return base64.b64decode(self.data.split(',')[1])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def device_state_attributes(self):
|
||||||
return f"{self.browserID}-camera"
|
return {
|
||||||
|
"type": "browser_mod",
|
||||||
@property
|
"deviceID": self.deviceID,
|
||||||
def entity_registry_visible_default(self):
|
"last_seen": self.last_seen,
|
||||||
return True
|
}
|
||||||
|
|
||||||
def camera_image(self, width=None, height=None):
|
|
||||||
if "camera" not in self._data:
|
|
||||||
return None
|
|
||||||
return base64.b64decode(self._data["camera"].split(",")[-1])
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import logging
|
|
||||||
from homeassistant import config_entries
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@config_entries.HANDLERS.register(DOMAIN)
|
|
||||||
class BrowserModConfigFlow(config_entries.ConfigFlow):
|
|
||||||
|
|
||||||
VERSION = 2
|
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
|
||||||
if self._async_current_entries():
|
|
||||||
return self.async_abort(reason="single_instance_allowed")
|
|
||||||
_LOGGER.error("Running async_create_entry")
|
|
||||||
return self.async_create_entry(title="Browser Mod", data={})
|
|
@ -1,199 +1,115 @@
|
|||||||
import logging
|
import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from homeassistant.components.websocket_api import (
|
from homeassistant.components.websocket_api import websocket_command, result_message, event_message, async_register_command
|
||||||
event_message,
|
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||||
async_register_command,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from .const import DOMAIN, WS_CONNECT, WS_UPDATE, WS_CAMERA
|
||||||
|
from .helpers import get_devices, create_entity, get_config
|
||||||
from homeassistant.core import callback
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
BROWSER_ID,
|
|
||||||
DATA_STORE,
|
|
||||||
WS_CONNECT,
|
|
||||||
WS_LOG,
|
|
||||||
WS_RECALL_ID,
|
|
||||||
WS_REGISTER,
|
|
||||||
WS_SETTINGS,
|
|
||||||
WS_UNREGISTER,
|
|
||||||
WS_UPDATE,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .browser import getBrowser, deleteBrowser, getBrowserByConnection
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def setup_connection(hass, config):
|
||||||
|
|
||||||
async def async_setup_connection(hass):
|
@websocket_command({
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_CONNECT,
|
vol.Required("type"): WS_CONNECT,
|
||||||
vol.Required("browserID"): str,
|
vol.Required("deviceID"): str,
|
||||||
}
|
})
|
||||||
)
|
def handle_connect(hass, connection, msg):
|
||||||
@websocket_api.async_response
|
deviceID = msg["deviceID"]
|
||||||
async def handle_connect(hass, connection, msg):
|
|
||||||
"""Connect to Browser Mod and subscribe to settings updates."""
|
|
||||||
browserID = msg[BROWSER_ID]
|
|
||||||
store = hass.data[DOMAIN][DATA_STORE]
|
|
||||||
|
|
||||||
@callback
|
device = get_devices(hass).get(deviceID, BrowserModConnection(hass, deviceID))
|
||||||
def send_update(data):
|
device.connect(connection, msg["id"])
|
||||||
connection.send_message(event_message(msg["id"], {"result": data}))
|
get_devices(hass)[deviceID] = device
|
||||||
|
|
||||||
store_listener = store.add_listener(send_update)
|
connection.send_message(result_message(msg["id"]))
|
||||||
|
|
||||||
def close_connection():
|
@websocket_command({
|
||||||
store_listener()
|
|
||||||
dev = getBrowser(hass, browserID, create=False)
|
|
||||||
if dev:
|
|
||||||
dev.close_connection(connection)
|
|
||||||
|
|
||||||
connection.subscriptions[msg["id"]] = close_connection
|
|
||||||
connection.send_result(msg["id"])
|
|
||||||
|
|
||||||
if store.get_browser(browserID).registered:
|
|
||||||
dev = getBrowser(hass, browserID)
|
|
||||||
dev.update_settings(hass, store.get_browser(browserID).asdict())
|
|
||||||
dev.open_connection(connection, msg["id"])
|
|
||||||
await store.set_browser(
|
|
||||||
browserID,
|
|
||||||
last_seen=datetime.now(tz=timezone.utc).isoformat(),
|
|
||||||
meta=dev.get_device_id(hass),
|
|
||||||
)
|
|
||||||
send_update(store.asdict())
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_REGISTER,
|
|
||||||
vol.Required("browserID"): str,
|
|
||||||
vol.Optional("data"): dict,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@websocket_api.async_response
|
|
||||||
async def handle_register(hass, connection, msg):
|
|
||||||
"""Register a Browser."""
|
|
||||||
browserID = msg[BROWSER_ID]
|
|
||||||
store = hass.data[DOMAIN][DATA_STORE]
|
|
||||||
|
|
||||||
browserSettings = {"registered": True}
|
|
||||||
data = msg.get("data", {})
|
|
||||||
if "last_seen" in data:
|
|
||||||
del data["last_seen"]
|
|
||||||
if BROWSER_ID in data:
|
|
||||||
# Change ID of registered browser
|
|
||||||
newBrowserID = data[BROWSER_ID]
|
|
||||||
del data[BROWSER_ID]
|
|
||||||
|
|
||||||
# Copy data from old browser and delete it from store
|
|
||||||
if oldBrowserSettings := store.get_browser(browserID):
|
|
||||||
browserSettings = oldBrowserSettings.asdict()
|
|
||||||
await store.delete_browser(browserID)
|
|
||||||
|
|
||||||
# Delete the old Browser device
|
|
||||||
deleteBrowser(hass, browserID)
|
|
||||||
|
|
||||||
# Use the new browserID from now on
|
|
||||||
browserID = newBrowserID
|
|
||||||
|
|
||||||
# Create and/or update Browser device
|
|
||||||
dev = getBrowser(hass, browserID)
|
|
||||||
dev.update_settings(hass, data)
|
|
||||||
|
|
||||||
# Create or update store data
|
|
||||||
if data is not None:
|
|
||||||
browserSettings.update(data)
|
|
||||||
await store.set_browser(browserID, **browserSettings)
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_UNREGISTER,
|
|
||||||
vol.Required("browserID"): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@websocket_api.async_response
|
|
||||||
async def handle_unregister(hass, connection, msg):
|
|
||||||
"""Unregister a Browser."""
|
|
||||||
browserID = msg[BROWSER_ID]
|
|
||||||
store = hass.data[DOMAIN][DATA_STORE]
|
|
||||||
|
|
||||||
deleteBrowser(hass, browserID)
|
|
||||||
await store.delete_browser(browserID)
|
|
||||||
|
|
||||||
connection.send_result(msg["id"])
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_UPDATE,
|
vol.Required("type"): WS_UPDATE,
|
||||||
vol.Required("browserID"): str,
|
vol.Required("deviceID"): str,
|
||||||
vol.Optional("data"): dict,
|
vol.Optional("data"): dict,
|
||||||
}
|
})
|
||||||
)
|
def handle_update( hass, connection, msg):
|
||||||
@websocket_api.async_response
|
devices = get_devices(hass)
|
||||||
async def handle_update(hass, connection, msg):
|
deviceID = msg["deviceID"]
|
||||||
"""Receive state updates from a Browser."""
|
if deviceID in devices:
|
||||||
browserID = msg[BROWSER_ID]
|
devices[deviceID].update(msg.get("data", None))
|
||||||
store = hass.data[DOMAIN][DATA_STORE]
|
|
||||||
|
|
||||||
if store.get_browser(browserID).registered:
|
|
||||||
dev = getBrowser(hass, browserID)
|
|
||||||
dev.update(hass, msg.get("data", {}))
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_SETTINGS,
|
|
||||||
vol.Required("key"): str,
|
|
||||||
vol.Optional("value"): vol.Any(int, str, bool, list, object, None),
|
|
||||||
vol.Optional("user"): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@websocket_api.async_response
|
|
||||||
async def handle_settings(hass, connection, msg):
|
|
||||||
"""Change user or global settings."""
|
|
||||||
store = hass.data[DOMAIN][DATA_STORE]
|
|
||||||
if "user" in msg:
|
|
||||||
# Set user setting
|
|
||||||
await store.set_user_settings(
|
|
||||||
msg["user"], **{msg["key"]: msg.get("value", None)}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Set global setting
|
|
||||||
await store.set_global_settings(**{msg["key"]: msg.get("value", None)})
|
|
||||||
pass
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_RECALL_ID,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def handle_recall_id(hass, connection, msg):
|
|
||||||
"""Recall browserID of Browser with the current connection."""
|
|
||||||
dev = getBrowserByConnection(hass, connection)
|
|
||||||
if dev:
|
|
||||||
connection.send_message(
|
|
||||||
websocket_api.result_message(msg["id"], dev.browserID)
|
|
||||||
)
|
|
||||||
connection.send_message(websocket_api.result_message(msg["id"], None))
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
|
||||||
{
|
|
||||||
vol.Required("type"): WS_LOG,
|
|
||||||
vol.Required("message"): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def handle_log(hass, connection, msg):
|
|
||||||
"""Print a debug message."""
|
|
||||||
_LOGGER.info(f"LOG MESSAGE: {msg['message']}")
|
|
||||||
|
|
||||||
async_register_command(hass, handle_connect)
|
async_register_command(hass, handle_connect)
|
||||||
async_register_command(hass, handle_register)
|
|
||||||
async_register_command(hass, handle_unregister)
|
|
||||||
async_register_command(hass, handle_update)
|
async_register_command(hass, handle_update)
|
||||||
async_register_command(hass, handle_settings)
|
|
||||||
async_register_command(hass, handle_recall_id)
|
class BrowserModConnection:
|
||||||
async_register_command(hass, handle_log)
|
def __init__(self, hass, deviceID):
|
||||||
|
self.hass = hass
|
||||||
|
self.deviceID = deviceID
|
||||||
|
self.connection = []
|
||||||
|
|
||||||
|
self.media_player = None
|
||||||
|
self.screen = None
|
||||||
|
self.sensor = None
|
||||||
|
self.fully = None
|
||||||
|
self.camera = None
|
||||||
|
|
||||||
|
def connect(self, connection, cid):
|
||||||
|
self.connection.append((connection, cid))
|
||||||
|
self.send("update", **get_config(self.hass, self.deviceID))
|
||||||
|
|
||||||
|
def disconnect():
|
||||||
|
self.connection.remove((connection, cid))
|
||||||
|
|
||||||
|
connection.subscriptions[cid] = disconnect
|
||||||
|
|
||||||
|
def send(self, command, **kwargs):
|
||||||
|
if self.connection:
|
||||||
|
connection, cid = self.connection[-1]
|
||||||
|
connection.send_message(event_message(cid, {
|
||||||
|
"command": command,
|
||||||
|
**kwargs,
|
||||||
|
}))
|
||||||
|
|
||||||
|
def update(self, data):
|
||||||
|
if data.get('browser'):
|
||||||
|
self.sensor = self.sensor or create_entity(
|
||||||
|
self.hass,
|
||||||
|
'sensor',
|
||||||
|
self.deviceID,
|
||||||
|
self)
|
||||||
|
if self.sensor:
|
||||||
|
self.sensor.data = data.get('browser')
|
||||||
|
|
||||||
|
if data.get('player'):
|
||||||
|
self.media_player = self.media_player or create_entity(
|
||||||
|
self.hass,
|
||||||
|
'media_player',
|
||||||
|
self.deviceID,
|
||||||
|
self)
|
||||||
|
if self.media_player:
|
||||||
|
self.media_player.data = data.get('player')
|
||||||
|
|
||||||
|
if data.get('screen'):
|
||||||
|
self.screen = self.screen or create_entity(
|
||||||
|
self.hass,
|
||||||
|
'light',
|
||||||
|
self.deviceID,
|
||||||
|
self)
|
||||||
|
if self.screen:
|
||||||
|
self.screen.data = data.get('screen')
|
||||||
|
|
||||||
|
if data.get('fully'):
|
||||||
|
self.fully = self.fully or create_entity(
|
||||||
|
self.hass,
|
||||||
|
'binary_sensor',
|
||||||
|
self.deviceID,
|
||||||
|
self)
|
||||||
|
if self.fully:
|
||||||
|
self.fully.data = data.get('fully')
|
||||||
|
|
||||||
|
if data.get('camera'):
|
||||||
|
self.camera = self.camera or create_entity(
|
||||||
|
self.hass,
|
||||||
|
'camera',
|
||||||
|
self.deviceID,
|
||||||
|
self)
|
||||||
|
if self.camera:
|
||||||
|
self.camera.data = data.get('camera')
|
||||||
|
|
||||||
|
@ -1,31 +1,33 @@
|
|||||||
DOMAIN = "browser_mod"
|
DOMAIN = "browser_mod"
|
||||||
|
|
||||||
BROWSER_ID = "browserID"
|
|
||||||
|
|
||||||
FRONTEND_SCRIPT_URL = "/browser_mod.js"
|
FRONTEND_SCRIPT_URL = "/browser_mod.js"
|
||||||
SETTINGS_PANEL_URL = "/browser_mod_panel.js"
|
|
||||||
|
|
||||||
DATA_BROWSERS = "browsers"
|
DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url'
|
||||||
|
|
||||||
|
DATA_DEVICES = "devices"
|
||||||
|
DATA_ALIASES = "aliases"
|
||||||
DATA_ADDERS = "adders"
|
DATA_ADDERS = "adders"
|
||||||
DATA_STORE = "store"
|
DATA_CONFIG = "config"
|
||||||
|
|
||||||
|
CONFIG_DEVICES = "devices"
|
||||||
|
CONFIG_PREFIX = "prefix"
|
||||||
|
CONFIG_DISABLE = "disable"
|
||||||
|
CONFIG_DISABLE_ALL = "all"
|
||||||
|
|
||||||
WS_ROOT = DOMAIN
|
WS_ROOT = DOMAIN
|
||||||
WS_CONNECT = f"{WS_ROOT}/connect"
|
WS_CONNECT = "{}/connect".format(WS_ROOT)
|
||||||
WS_REGISTER = f"{WS_ROOT}/register"
|
WS_UPDATE = "{}/update".format(WS_ROOT)
|
||||||
WS_UNREGISTER = f"{WS_ROOT}/unregister"
|
WS_CAMERA = "{}/camera".format(WS_ROOT)
|
||||||
WS_UPDATE = f"{WS_ROOT}/update"
|
|
||||||
WS_SETTINGS = f"{WS_ROOT}/settings"
|
|
||||||
WS_RECALL_ID = f"{WS_ROOT}/recall_id"
|
|
||||||
WS_LOG = f"{WS_ROOT}/log"
|
|
||||||
|
|
||||||
BROWSER_MOD_SERVICES = [
|
USER_COMMANDS = [
|
||||||
"sequence",
|
"debug",
|
||||||
"delay",
|
|
||||||
"popup",
|
"popup",
|
||||||
"more_info",
|
"close-popup",
|
||||||
"close_popup",
|
|
||||||
"navigate",
|
"navigate",
|
||||||
"refresh",
|
"more-info",
|
||||||
"console",
|
"set-theme",
|
||||||
"javascript",
|
"lovelace-reload",
|
||||||
]
|
"blackout",
|
||||||
|
"no-blackout",
|
||||||
|
"toast",
|
||||||
|
]
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserModEntity(CoordinatorEntity):
|
|
||||||
def __init__(self, coordinator, browserID, name, icon=None):
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.browserID = browserID
|
|
||||||
self._name = name
|
|
||||||
self._icon = icon
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _data(self):
|
|
||||||
return self.coordinator.data or {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_info(self):
|
|
||||||
config_url = {}
|
|
||||||
if ip := self._data.get("browser", {}).get("ip_address"):
|
|
||||||
config_url = {"configuration_url": f"http://{ip}:2323"}
|
|
||||||
return {
|
|
||||||
"identifiers": {(DOMAIN, self.browserID)},
|
|
||||||
"name": self.browserID,
|
|
||||||
"manufacturer": "Browser Mod",
|
|
||||||
**config_url,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self):
|
|
||||||
return {
|
|
||||||
"type": "browser_mod",
|
|
||||||
"browserID": self.browserID,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_entity_name(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def entity_registry_visible_default(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unique_id(self):
|
|
||||||
return f"{self.browserID}-{self._name.replace(' ','_')}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def icon(self):
|
|
||||||
return self._icon
|
|
68
custom_components/browser_mod/helpers.py
Normal file
68
custom_components/browser_mod/helpers.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||||
|
|
||||||
|
from .const import DOMAIN, DATA_DEVICES, DATA_ALIASES, DATA_ADDERS, CONFIG_DEVICES, DATA_CONFIG, CONFIG_PREFIX, CONFIG_DISABLE, CONFIG_DISABLE_ALL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_devices(hass):
|
||||||
|
return hass.data[DOMAIN][DATA_DEVICES]
|
||||||
|
|
||||||
|
def get_alias(hass, deviceID):
|
||||||
|
for k,v in hass.data[DOMAIN][DATA_ALIASES].items():
|
||||||
|
if v == deviceID:
|
||||||
|
return k
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_config(hass, deviceID):
|
||||||
|
config = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DEVICES, {})
|
||||||
|
return config.get(deviceID, config.get(deviceID.replace('-','_'), {}))
|
||||||
|
|
||||||
|
def create_entity(hass, platform, deviceID, connection):
|
||||||
|
conf = get_config(hass, deviceID)
|
||||||
|
if conf and (platform in conf.get(CONFIG_DISABLE, [])
|
||||||
|
or CONFIG_DISABLE_ALL in conf.get(CONFIG_DISABLE, [])):
|
||||||
|
return None
|
||||||
|
if not conf and (platform in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, [])
|
||||||
|
or CONFIG_DISABLE_ALL in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, [])):
|
||||||
|
return None
|
||||||
|
adder = hass.data[DOMAIN][DATA_ADDERS][platform]
|
||||||
|
entity = adder(hass, deviceID, connection, get_alias(hass, deviceID))
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def setup_platform(hass, config, async_add_devices, platform, cls):
|
||||||
|
def adder(hass, deviceID, connection, alias=None):
|
||||||
|
entity = cls(hass, connection, deviceID, alias)
|
||||||
|
async_add_devices([entity])
|
||||||
|
return entity
|
||||||
|
hass.data[DOMAIN][DATA_ADDERS][platform] = adder
|
||||||
|
return True
|
||||||
|
|
||||||
|
class BrowserModEntity(Entity):
|
||||||
|
|
||||||
|
def __init__(self, hass, connection, deviceID, alias=None):
|
||||||
|
self.hass = hass
|
||||||
|
self.connection = connection
|
||||||
|
self.deviceID = deviceID
|
||||||
|
self._data = {}
|
||||||
|
prefix = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_PREFIX, '')
|
||||||
|
self.entity_id = async_generate_entity_id(self.domain+".{}", alias or f"{prefix}{deviceID}", hass=hass)
|
||||||
|
|
||||||
|
def updated(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
@data.setter
|
||||||
|
def data(self, data):
|
||||||
|
self._data = data
|
||||||
|
self.updated()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_id(self):
|
||||||
|
return self.deviceID
|
||||||
|
|
||||||
|
def send(self, command, **kwargs):
|
||||||
|
self.connection.send(command, **kwargs)
|
@ -1,47 +1,55 @@
|
|||||||
from homeassistant.components.light import LightEntity, ColorMode
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .entities import BrowserModEntity
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF
|
||||||
from .const import DOMAIN, DATA_ADDERS
|
from homeassistant.components.light import Light, SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
|
from .helpers import setup_platform, BrowserModEntity
|
||||||
|
|
||||||
async def async_setup_platform(
|
PLATFORM = 'light'
|
||||||
hass, config_entry, async_add_entities, discoveryInfo=None
|
|
||||||
):
|
|
||||||
hass.data[DOMAIN][DATA_ADDERS]["light"] = async_add_entities
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModLight)
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
class BrowserModLight(Light, BrowserModEntity):
|
||||||
await async_setup_platform(hass, {}, async_add_entities)
|
domain = PLATFORM
|
||||||
|
|
||||||
|
def __init__(self, hass, connection, deviceID, alias=None):
|
||||||
|
super().__init__(hass, connection, deviceID, alias)
|
||||||
|
self.last_seen = None
|
||||||
|
|
||||||
class BrowserModLight(BrowserModEntity, LightEntity):
|
def updated(self):
|
||||||
def __init__(self, coordinator, browserID, browser):
|
self.last_seen = datetime.now()
|
||||||
BrowserModEntity.__init__(self, coordinator, browserID, "Screen")
|
self.schedule_update_ha_state()
|
||||||
LightEntity.__init__(self)
|
|
||||||
self.browser = browser
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_registry_visible_default(self):
|
def state(self):
|
||||||
return True
|
if not self.connection.connection:
|
||||||
|
return STATE_UNAVAILABLE
|
||||||
|
if self.data.get('blackout', False):
|
||||||
|
return STATE_OFF
|
||||||
|
return STATE_ON
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
return self._data.get("screen_on", None)
|
return not self.data.get('blackout', False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_color_modes(self):
|
def device_state_attributes(self):
|
||||||
return {ColorMode.BRIGHTNESS}
|
return {
|
||||||
|
"type": "browser_mod",
|
||||||
|
"deviceID": self.deviceID,
|
||||||
|
"last_seen": self.last_seen,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_mode(self):
|
def supported_features(self):
|
||||||
return ColorMode.BRIGHTNESS
|
if self.data.get('brightness', False):
|
||||||
|
return SUPPORT_BRIGHTNESS
|
||||||
|
return 0
|
||||||
|
|
||||||
@property
|
def turn_on(self, **kwargs):
|
||||||
def brightness(self):
|
self.connection.send("no-blackout", **kwargs)
|
||||||
return self._data.get("screen_brightness", 1)
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
await self.browser.send("screen_on", **kwargs)
|
self.connection.send("blackout")
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs):
|
|
||||||
await self.browser.send("screen_off")
|
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
{
|
{
|
||||||
"domain": "browser_mod",
|
"domain": "browser_mod",
|
||||||
"name": "Browser mod",
|
"name": "Browser mod",
|
||||||
"documentation": "https://github.com/thomasloven/hass-browser_mod/blob/master/README.md",
|
"documentation": "",
|
||||||
"dependencies": ["panel_custom", "websocket_api", "http", "frontend", "lovelace"],
|
"dependencies": ["websocket_api", "http"],
|
||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"requirements": [],
|
"requirements": []
|
||||||
"version": "2.0.0",
|
|
||||||
"iot_class": "local_push",
|
|
||||||
"config_flow": true
|
|
||||||
}
|
}
|
||||||
|
@ -1,155 +1,82 @@
|
|||||||
from homeassistant.components import media_source
|
import logging
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_PLAY,
|
SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PLAY_MEDIA,
|
SUPPORT_PAUSE, SUPPORT_STOP,
|
||||||
SUPPORT_PAUSE,
|
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_STOP,
|
MediaPlayerDevice,
|
||||||
SUPPORT_VOLUME_SET,
|
)
|
||||||
SUPPORT_VOLUME_MUTE,
|
|
||||||
MediaPlayerEntity,
|
|
||||||
)
|
|
||||||
from homeassistant.components.media_player.browse_media import (
|
|
||||||
async_process_play_media_url,
|
|
||||||
)
|
|
||||||
from homeassistant.components.media_player.const import (
|
|
||||||
MEDIA_TYPE_MUSIC,
|
|
||||||
MEDIA_TYPE_URL,
|
|
||||||
SUPPORT_BROWSE_MEDIA,
|
|
||||||
SUPPORT_SEEK,
|
|
||||||
SUPPORT_TURN_OFF,
|
|
||||||
SUPPORT_TURN_ON,
|
|
||||||
)
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
STATE_PAUSED,
|
STATE_PAUSED,
|
||||||
STATE_PLAYING,
|
STATE_PLAYING,
|
||||||
STATE_IDLE,
|
STATE_IDLE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
STATE_ON,
|
)
|
||||||
STATE_OFF,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.util import dt
|
from .helpers import setup_platform, BrowserModEntity
|
||||||
|
|
||||||
from .entities import BrowserModEntity
|
_LOGGER = logging.getLogger(__name__)
|
||||||
from .const import DOMAIN, DATA_ADDERS
|
|
||||||
|
PLATFORM = 'media_player'
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModPlayer)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
class BrowserModPlayer(MediaPlayerDevice, BrowserModEntity):
|
||||||
hass, config_entry, async_add_entities, discoveryInfo=None
|
domain = PLATFORM
|
||||||
):
|
|
||||||
hass.data[DOMAIN][DATA_ADDERS]["media_player"] = async_add_entities
|
|
||||||
|
|
||||||
|
def __init__(self, hass, connection, deviceID, alias=None):
|
||||||
|
super().__init__(hass, connection, deviceID, alias)
|
||||||
|
self.last_seen = None
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
def updated(self):
|
||||||
await async_setup_platform(hass, {}, async_add_entities)
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class BrowserModPlayer(BrowserModEntity, MediaPlayerEntity):
|
|
||||||
def __init__(self, coordinator, browserID, browser):
|
|
||||||
BrowserModEntity.__init__(self, coordinator, browserID, None)
|
|
||||||
MediaPlayerEntity.__init__(self)
|
|
||||||
self.browser = browser
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def device_state_attributes(self):
|
||||||
return f"{self.browserID}-player"
|
return {
|
||||||
|
"type": "browser_mod",
|
||||||
@property
|
"deviceID": self.deviceID,
|
||||||
def entity_registry_visible_default(self):
|
}
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
state = self._data.get("player", {}).get("state")
|
if not self.connection.connection:
|
||||||
|
return STATE_UNAVAILABLE
|
||||||
|
state = self.data.get("state", "unknown")
|
||||||
return {
|
return {
|
||||||
"playing": STATE_PLAYING,
|
"playing": STATE_PLAYING,
|
||||||
"paused": STATE_PAUSED,
|
"paused": STATE_PAUSED,
|
||||||
"stopped": STATE_IDLE,
|
"stopped": STATE_IDLE,
|
||||||
"unavailable": STATE_UNAVAILABLE,
|
|
||||||
"on": STATE_ON,
|
|
||||||
"off": STATE_OFF,
|
|
||||||
}.get(state, STATE_UNKNOWN)
|
}.get(state, STATE_UNKNOWN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
return (
|
return (
|
||||||
SUPPORT_PLAY
|
SUPPORT_PLAY | SUPPORT_PLAY_MEDIA |
|
||||||
| SUPPORT_PLAY_MEDIA
|
SUPPORT_PAUSE | SUPPORT_STOP |
|
||||||
| SUPPORT_PAUSE
|
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
|
||||||
| SUPPORT_STOP
|
|
||||||
| SUPPORT_VOLUME_SET
|
|
||||||
| SUPPORT_VOLUME_MUTE
|
|
||||||
| SUPPORT_BROWSE_MEDIA
|
|
||||||
| SUPPORT_SEEK
|
|
||||||
| SUPPORT_TURN_OFF
|
|
||||||
| SUPPORT_TURN_ON
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
return self._data.get("player", {}).get("volume", 0)
|
return self.data.get("volume", 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_volume_muted(self):
|
def is_volume_muted(self):
|
||||||
return self._data.get("player", {}).get("muted", False)
|
return self.data.get("muted", False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_duration(self):
|
def media_content_id(self):
|
||||||
duration = self._data.get("player", {}).get("media_duration", None)
|
return self.data.get("src", "")
|
||||||
return float(duration) if duration is not None else None
|
|
||||||
|
|
||||||
@property
|
def set_volume_level(self, volume):
|
||||||
def media_position(self):
|
self.connection.send("set_volume", volume_level=volume)
|
||||||
position = self._data.get("player", {}).get("media_position", None)
|
def mute_volume(self, mute):
|
||||||
return float(position) if position is not None else None
|
self.connection.send("mute", mute=mute)
|
||||||
|
|
||||||
@property
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
def media_position_updated_at(self):
|
self.connection.send("play", media_content_id=media_id)
|
||||||
return dt.utcnow()
|
def media_play(self):
|
||||||
|
self.connection.send("play")
|
||||||
async def async_set_volume_level(self, volume):
|
def media_pause(self):
|
||||||
await self.browser.send("player-set-volume", volume_level=volume)
|
self.connection.send("pause")
|
||||||
|
def media_stop(self):
|
||||||
async def async_mute_volume(self, mute):
|
self.connection.send("stop")
|
||||||
await self.browser.send("player-mute", mute=mute)
|
|
||||||
|
|
||||||
async def async_play_media(self, media_type, media_id, **kwargs):
|
|
||||||
if media_source.is_media_source_id(media_id):
|
|
||||||
play_item = await media_source.async_resolve_media(
|
|
||||||
self.hass, media_id, self.entity_id
|
|
||||||
)
|
|
||||||
media_type = play_item.mime_type
|
|
||||||
media_id = play_item.url
|
|
||||||
media_id = async_process_play_media_url(self.hass, media_id)
|
|
||||||
if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC):
|
|
||||||
media_id = async_process_play_media_url(self.hass, media_id)
|
|
||||||
await self.browser.send(
|
|
||||||
"player-play", media_content_id=media_id, media_type=media_type, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
|
||||||
"""Implement the websocket media browsing helper."""
|
|
||||||
return await media_source.async_browse_media(
|
|
||||||
self.hass,
|
|
||||||
media_content_id,
|
|
||||||
# content_filter=lambda item: item.media_content_type.startswith("audio/"),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_media_play(self):
|
|
||||||
await self.browser.send("player-play")
|
|
||||||
|
|
||||||
async def async_media_pause(self):
|
|
||||||
await self.browser.send("player-pause")
|
|
||||||
|
|
||||||
async def async_media_stop(self):
|
|
||||||
await self.browser.send("player-stop")
|
|
||||||
|
|
||||||
async def async_media_seek(self, position):
|
|
||||||
await self.browser.send("player-seek", position=position)
|
|
||||||
|
|
||||||
async def async_turn_off(self):
|
|
||||||
await self.browser.send("player-turn-off")
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs):
|
|
||||||
await self.browser.send("player-turn-on", **kwargs)
|
|
||||||
|
@ -1,71 +1,36 @@
|
|||||||
from homeassistant.components.frontend import add_extra_js_url
|
from aiohttp import web
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
from .const import FRONTEND_SCRIPT_URL, SETTINGS_PANEL_URL
|
from .const import FRONTEND_SCRIPT_URL, DATA_EXTRA_MODULE_URL
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_view(hass):
|
def setup_view(hass):
|
||||||
|
if DATA_EXTRA_MODULE_URL not in hass.data:
|
||||||
|
hass.data[DATA_EXTRA_MODULE_URL] = set()
|
||||||
|
url_set = hass.data[DATA_EXTRA_MODULE_URL]
|
||||||
|
url_set.add(FRONTEND_SCRIPT_URL)
|
||||||
|
|
||||||
# Serve the Browser Mod controller and add it as extra_module_url
|
hass.http.register_view(ModView(hass, FRONTEND_SCRIPT_URL))
|
||||||
hass.http.register_static_path(
|
|
||||||
FRONTEND_SCRIPT_URL,
|
|
||||||
hass.config.path("custom_components/browser_mod/browser_mod.js"),
|
|
||||||
)
|
|
||||||
add_extra_js_url(hass, FRONTEND_SCRIPT_URL)
|
|
||||||
|
|
||||||
# Serve the Browser Mod Settings panel and register it as a panel
|
class ModView(HomeAssistantView):
|
||||||
hass.http.register_static_path(
|
|
||||||
SETTINGS_PANEL_URL,
|
|
||||||
hass.config.path("custom_components/browser_mod/browser_mod_panel.js"),
|
|
||||||
)
|
|
||||||
hass.components.frontend.async_register_built_in_panel(
|
|
||||||
component_name="custom",
|
|
||||||
sidebar_title="Browser Mod",
|
|
||||||
sidebar_icon="mdi:server",
|
|
||||||
frontend_url_path="browser-mod",
|
|
||||||
require_admin=True,
|
|
||||||
config={
|
|
||||||
"_panel_custom": {
|
|
||||||
"name": "browser-mod-panel",
|
|
||||||
"js_url": SETTINGS_PANEL_URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also load Browser Mod as a lovelace resource so it's accessible to Cast
|
name = "browser_mod_script"
|
||||||
resources = hass.data["lovelace"]["resources"]
|
requires_auth = False
|
||||||
if resources:
|
|
||||||
if not resources.loaded:
|
|
||||||
await resources.async_load()
|
|
||||||
resources.loaded = True
|
|
||||||
|
|
||||||
frontend_added = False
|
def __init__(self, hass, url):
|
||||||
for r in resources.async_items():
|
self.url = url
|
||||||
if r["url"].startswith(FRONTEND_SCRIPT_URL):
|
self.config_dir = hass.config.path()
|
||||||
frontend_added = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# While going through the resources, also preload card-mod if it is found
|
async def get(self, request):
|
||||||
if "card-mod.js" in r["url"]:
|
path = "{}/custom_components/browser_mod/browser_mod.js".format(self.config_dir)
|
||||||
add_extra_js_url(hass, r["url"])
|
|
||||||
|
|
||||||
if not frontend_added:
|
filecontent = ""
|
||||||
if getattr(resources, "async_create_item", None):
|
|
||||||
await resources.async_create_item(
|
try:
|
||||||
{
|
with open(path, mode="r", encoding="utf-8", errors="ignore") as localfile:
|
||||||
"res_type": "module",
|
filecontent = localfile.read()
|
||||||
"url": FRONTEND_SCRIPT_URL + "?automatically-added",
|
localfile.close()
|
||||||
}
|
except Exception as exception:
|
||||||
)
|
pass
|
||||||
elif getattr(resources, "data", None) and getattr(
|
|
||||||
resources.data, "append", None
|
return web.Response(body=filecontent, content_type="text/javascript", charset="utf-8")
|
||||||
):
|
|
||||||
resources.data.append(
|
|
||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"url": FRONTEND_SCRIPT_URL + "?automatically-added",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
@ -1,64 +1,37 @@
|
|||||||
from homeassistant.components.sensor import SensorEntity
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .const import DOMAIN, DATA_ADDERS
|
from homeassistant.const import STATE_UNAVAILABLE
|
||||||
from .entities import BrowserModEntity
|
|
||||||
|
|
||||||
|
from .helpers import setup_platform, BrowserModEntity
|
||||||
|
|
||||||
async def async_setup_platform(
|
PLATFORM = 'sensor'
|
||||||
hass, config_entry, async_add_entities, discoveryInfo=None
|
|
||||||
):
|
|
||||||
hass.data[DOMAIN][DATA_ADDERS]["sensor"] = async_add_entities
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
|
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor)
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
class BrowserModSensor(BrowserModEntity):
|
||||||
await async_setup_platform(hass, {}, async_add_entities)
|
domain = PLATFORM
|
||||||
|
|
||||||
|
def __init__(self, hass, connection, deviceID, alias=None):
|
||||||
|
super().__init__(hass, connection, deviceID, alias)
|
||||||
|
self.last_seen = None
|
||||||
|
|
||||||
class BrowserSensor(BrowserModEntity, SensorEntity):
|
def updated(self):
|
||||||
def __init__(
|
self.last_seen = datetime.now()
|
||||||
self,
|
self.schedule_update_ha_state()
|
||||||
coordinator,
|
|
||||||
browserID,
|
|
||||||
parameter,
|
|
||||||
name,
|
|
||||||
unit_of_measurement=None,
|
|
||||||
device_class=None,
|
|
||||||
icon=None,
|
|
||||||
):
|
|
||||||
BrowserModEntity.__init__(self, coordinator, browserID, name, icon)
|
|
||||||
SensorEntity.__init__(self)
|
|
||||||
self.parameter = parameter
|
|
||||||
self._device_class = device_class
|
|
||||||
self._unit_of_measurement = unit_of_measurement
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def state(self):
|
||||||
val = self._data.get("browser", {}).get(self.parameter, None)
|
if not self.connection.connection:
|
||||||
if len(str(val)) > 255:
|
return STATE_UNAVAILABLE
|
||||||
val = str(val)[:250] + "..."
|
return len(self.connection.connection)
|
||||||
return val
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_state_attributes(self):
|
||||||
return self._device_class
|
return {
|
||||||
|
"type": "browser_mod",
|
||||||
@property
|
"last_seen": self.last_seen,
|
||||||
def native_unit_of_measurement(self):
|
"deviceID": self.deviceID,
|
||||||
return self._unit_of_measurement
|
**self.data
|
||||||
|
}
|
||||||
@property
|
|
||||||
def extra_state_attributes(self):
|
|
||||||
retval = super().extra_state_attributes
|
|
||||||
|
|
||||||
if self.parameter == "currentUser":
|
|
||||||
retval["userData"] = self._data.get("browser", {}).get("userData")
|
|
||||||
|
|
||||||
if self.parameter == "path":
|
|
||||||
retval["pathSegments"] = (
|
|
||||||
self._data.get("browser", {}).get("path", "").split("/")
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.parameter == "userAgent":
|
|
||||||
retval["userAgent"] = self._data.get("browser", {}).get("userAgent")
|
|
||||||
|
|
||||||
return retval
|
|
||||||
|
@ -1,64 +1,37 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from .const import DOMAIN, DATA_DEVICES, DATA_ALIASES, USER_COMMANDS
|
||||||
from homeassistant.helpers import device_registry
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
BROWSER_MOD_SERVICES,
|
|
||||||
DOMAIN,
|
|
||||||
DATA_BROWSERS,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def setup_service(hass):
|
||||||
|
|
||||||
async def async_setup_services(hass):
|
def handle_command(call):
|
||||||
def call_service(service, targets, data):
|
command = call.data.get("command", None)
|
||||||
|
if not command:
|
||||||
browsers = hass.data[DOMAIN][DATA_BROWSERS]
|
return
|
||||||
|
|
||||||
|
targets = call.data.get("deviceID", None)
|
||||||
if isinstance(targets, str):
|
if isinstance(targets, str):
|
||||||
targets = [targets]
|
targets = [targets]
|
||||||
|
devices = hass.data[DOMAIN][DATA_DEVICES]
|
||||||
|
aliases = hass.data[DOMAIN][DATA_ALIASES]
|
||||||
|
if not targets:
|
||||||
|
targets = devices.keys()
|
||||||
|
targets = [aliases.get(t, t) for t in targets]
|
||||||
|
|
||||||
# If no targets were specified, send to all browsers
|
data = dict(call.data)
|
||||||
if len(targets) == 0:
|
del data["command"]
|
||||||
targets = browsers.keys()
|
|
||||||
|
|
||||||
for target in targets:
|
for t in targets:
|
||||||
if target not in browsers:
|
if t in devices:
|
||||||
continue
|
devices[t].send(command, **data)
|
||||||
browser = browsers[target]
|
|
||||||
hass.create_task(browser.send(service, **data))
|
|
||||||
|
|
||||||
def handle_service(call):
|
def command_wrapper(call):
|
||||||
service = call.service
|
command = call.service.replace('_','-')
|
||||||
data = {**call.data}
|
call.data = dict(call.data)
|
||||||
|
call.data['command'] = command
|
||||||
|
handle_command(call)
|
||||||
|
|
||||||
browsers = data.pop("browser_id", [])
|
hass.services.async_register(DOMAIN, 'command', handle_command)
|
||||||
if isinstance(browsers, str):
|
for cmd in USER_COMMANDS:
|
||||||
browsers = [browsers]
|
hass.services.async_register(DOMAIN, cmd.replace('-','_'), command_wrapper)
|
||||||
browsers = set(browsers)
|
|
||||||
device_ids = set(data.pop("device_id", []))
|
|
||||||
area_ids = set(data.pop("area_id", []))
|
|
||||||
|
|
||||||
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
|
|
||||||
browsers.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
|
|
||||||
browsers.add(browserID)
|
|
||||||
|
|
||||||
call_service(service, browsers, data)
|
|
||||||
|
|
||||||
for service in BROWSER_MOD_SERVICES:
|
|
||||||
hass.services.async_register(DOMAIN, service, handle_service)
|
|
||||||
|
@ -1,238 +1,9 @@
|
|||||||
sequence:
|
command:
|
||||||
description: "Run a sequence of services"
|
description: Send a command to a browser
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
fields:
|
fields:
|
||||||
sequence:
|
command:
|
||||||
name: Actions
|
description: Command to send
|
||||||
description: List of services to run
|
example: 'navigate'
|
||||||
selector:
|
deviceID:
|
||||||
object:
|
description: List of receiving browsers
|
||||||
|
example: '["99980b13-dabc9563", "office_computer"]'
|
||||||
delay:
|
|
||||||
description: "Wait for a time"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
fields:
|
|
||||||
time:
|
|
||||||
name: Time
|
|
||||||
description: Time to wait (ms)
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
|
|
||||||
popup:
|
|
||||||
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:
|
|
||||||
name: Title
|
|
||||||
description: "Popup title"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
content:
|
|
||||||
name: Content
|
|
||||||
required: true
|
|
||||||
description: "Popup content (Test or lovelace card configuration)"
|
|
||||||
selector:
|
|
||||||
object:
|
|
||||||
size:
|
|
||||||
name: Size
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
mode: dropdown
|
|
||||||
options:
|
|
||||||
- normal
|
|
||||||
- wide
|
|
||||||
- fullscreen
|
|
||||||
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:
|
|
||||||
autoclose:
|
|
||||||
name: Auto close
|
|
||||||
description: Close the popup automatically on mouse, pointer or keyboard activity
|
|
||||||
default: false
|
|
||||||
selector:
|
|
||||||
boolean:
|
|
||||||
timeout:
|
|
||||||
name: Auto close timeout
|
|
||||||
description: Time before closing (ms)
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
mode: box
|
|
||||||
timeout_action:
|
|
||||||
name: Timeout action
|
|
||||||
description: Action to perform when popup is closed by timeout
|
|
||||||
selector:
|
|
||||||
object:
|
|
||||||
style:
|
|
||||||
name: Styles
|
|
||||||
description: CSS code to apply to the popup window
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
multiline: true
|
|
||||||
|
|
||||||
more_info:
|
|
||||||
description: "Show more-info dialog"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
fields:
|
|
||||||
entity:
|
|
||||||
name: Entity ID
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
large:
|
|
||||||
name: Large size
|
|
||||||
default: false
|
|
||||||
selector:
|
|
||||||
boolean:
|
|
||||||
ignore_popup_card:
|
|
||||||
name: Ignore any active popup-card overrides
|
|
||||||
default: false
|
|
||||||
selector:
|
|
||||||
boolean:
|
|
||||||
|
|
||||||
close_popup:
|
|
||||||
description: "Close a popup"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
|
|
||||||
navigate:
|
|
||||||
description: "Navigate browser to a different page"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
fields:
|
|
||||||
path:
|
|
||||||
name: Path
|
|
||||||
description: "Target path"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
refresh:
|
|
||||||
description: "Refresh page"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
|
|
||||||
console:
|
|
||||||
description: "Print text to browser console"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
fields:
|
|
||||||
message:
|
|
||||||
name: Message
|
|
||||||
description: "Text to print"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
javascript:
|
|
||||||
description: "Run arbitrary JavaScript code"
|
|
||||||
target:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
entity:
|
|
||||||
integration: "browser_mod_none"
|
|
||||||
area:
|
|
||||||
device:
|
|
||||||
integration: "browser_mod"
|
|
||||||
multiple: true
|
|
||||||
fields:
|
|
||||||
code:
|
|
||||||
name: Code
|
|
||||||
description: "JavaScript code to run"
|
|
||||||
selector:
|
|
||||||
object:
|
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
import logging
|
|
||||||
import attr
|
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
|
||||||
STORAGE_KEY = "browser_mod.storage"
|
|
||||||
|
|
||||||
LISTENER_STORAGE_KEY = "browser_mod.config_listeners"
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class SettingsStoreData:
|
|
||||||
hideSidebar = attr.ib(type=bool, default=None)
|
|
||||||
hideHeader = attr.ib(type=bool, default=None)
|
|
||||||
defaultPanel = attr.ib(type=str, default=None)
|
|
||||||
sidebarPanelOrder = 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)
|
|
||||||
titleTemplate = attr.ib(type=str, default=None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
return cls(**data)
|
|
||||||
|
|
||||||
def asdict(self):
|
|
||||||
return attr.asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class BrowserStoreData:
|
|
||||||
last_seen = attr.ib(type=int, default=0)
|
|
||||||
registered = attr.ib(type=bool, default=False)
|
|
||||||
camera = attr.ib(type=bool, default=False)
|
|
||||||
settings = attr.ib(type=SettingsStoreData, factory=SettingsStoreData)
|
|
||||||
meta = attr.ib(type=str, default="default")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
settings = SettingsStoreData.from_dict(data.get("settings", {}))
|
|
||||||
return cls(
|
|
||||||
**(
|
|
||||||
data
|
|
||||||
| {
|
|
||||||
"settings": settings,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def asdict(self):
|
|
||||||
return attr.asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
|
||||||
class ConfigStoreData:
|
|
||||||
browsers = attr.ib(type=dict[str:BrowserStoreData], factory=dict)
|
|
||||||
version = attr.ib(type=str, default="2.0")
|
|
||||||
settings = attr.ib(type=SettingsStoreData, factory=SettingsStoreData)
|
|
||||||
user_settings = attr.ib(type=dict[str:SettingsStoreData], factory=dict)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data={}):
|
|
||||||
browsers = {
|
|
||||||
k: BrowserStoreData.from_dict(v)
|
|
||||||
for k, v in data.get("browsers", {}).items()
|
|
||||||
}
|
|
||||||
user_settings = {
|
|
||||||
k: SettingsStoreData.from_dict(v)
|
|
||||||
for k, v in data.get("user_settings", {}).items()
|
|
||||||
}
|
|
||||||
settings = SettingsStoreData.from_dict(data.get("settings", {}))
|
|
||||||
return cls(
|
|
||||||
**(
|
|
||||||
data
|
|
||||||
| {
|
|
||||||
"browsers": browsers,
|
|
||||||
"settings": settings,
|
|
||||||
"user_settings": user_settings,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def asdict(self):
|
|
||||||
return attr.asdict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class BrowserModStore:
|
|
||||||
def __init__(self, hass):
|
|
||||||
self.store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
|
||||||
self.listeners = []
|
|
||||||
self.data = None
|
|
||||||
self.dirty = False
|
|
||||||
|
|
||||||
async def save(self):
|
|
||||||
if self.dirty:
|
|
||||||
await self.store.async_save(attr.asdict(self.data))
|
|
||||||
self.dirty = False
|
|
||||||
|
|
||||||
async def load(self):
|
|
||||||
stored = await self.store.async_load()
|
|
||||||
if stored:
|
|
||||||
self.data = ConfigStoreData.from_dict(stored)
|
|
||||||
if self.data is None:
|
|
||||||
self.data = ConfigStoreData()
|
|
||||||
await self.save()
|
|
||||||
self.dirty = False
|
|
||||||
|
|
||||||
async def updated(self):
|
|
||||||
self.dirty = True
|
|
||||||
for listener in self.listeners:
|
|
||||||
listener(attr.asdict(self.data))
|
|
||||||
await self.save()
|
|
||||||
|
|
||||||
def asdict(self):
|
|
||||||
return self.data.asdict()
|
|
||||||
|
|
||||||
def add_listener(self, callback):
|
|
||||||
self.listeners.append(callback)
|
|
||||||
|
|
||||||
def remove_listener():
|
|
||||||
self.listeners.remove(callback)
|
|
||||||
|
|
||||||
return remove_listener
|
|
||||||
|
|
||||||
def get_browser(self, browserID):
|
|
||||||
return self.data.browsers.get(browserID, BrowserStoreData())
|
|
||||||
|
|
||||||
async def set_browser(self, browserID, **data):
|
|
||||||
browser = self.data.browsers.get(browserID, BrowserStoreData())
|
|
||||||
browser.__dict__.update(data)
|
|
||||||
self.data.browsers[browserID] = browser
|
|
||||||
await self.updated()
|
|
||||||
|
|
||||||
async def delete_browser(self, browserID):
|
|
||||||
del self.data.browsers[browserID]
|
|
||||||
await self.updated()
|
|
||||||
|
|
||||||
def get_user_settings(self, name):
|
|
||||||
return self.data.user_settings.get(name, SettingsStoreData())
|
|
||||||
|
|
||||||
async def set_user_settings(self, name, **data):
|
|
||||||
settings = self.data.user_settings.get(name, SettingsStoreData())
|
|
||||||
settings.__dict__.update(data)
|
|
||||||
self.data.user_settings[name] = settings
|
|
||||||
await self.updated()
|
|
||||||
|
|
||||||
def get_global_settings(self):
|
|
||||||
return self.data.settings
|
|
||||||
|
|
||||||
async def set_global_settings(self, **data):
|
|
||||||
self.data.settings.__dict__.update(data)
|
|
||||||
await self.updated()
|
|
@ -1,109 +0,0 @@
|
|||||||
# The Browser Mod Configuration Panel
|
|
||||||
|
|
||||||
## This browser
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
LocalStorage works basically like cookies in that the information is stored locally on your device. Unlike a cookie, though, the information is bound to a URL. Therefore you may get different `BrowserID`s in the same browser if you e.g. access Home Assistant through different URLs inside and outside of your LAN, or through Home Assistant Cloud.
|
|
||||||
|
|
||||||
### Register
|
|
||||||
|
|
||||||
Registering a _Browser_ as a device will create a Home Assistant Device associated with that browser. The device has the following entities:
|
|
||||||
|
|
||||||
- A `media_player` entitiy which will play sound and video through the browser.
|
|
||||||
- A `light` entity will turn the screen on or off and controll the brightness if you are using [Fully Kiosk Browser](https://www.fully-kiosk.com/) (FKB). If you are not using FKB the function will be simulated by covering the screen with a black (or semitransparent) box.
|
|
||||||
- A motion `binary_sensor` which reacts to mouse and/or keyboard activity in the Browser. In FKB this can also react to motion in front of the devices camera.
|
|
||||||
- A number of `sensor` and `binary_sensor` entities providing different bits of information about the Browser which you may or may not find useful.
|
|
||||||
|
|
||||||
Registering a browser also enables it to act as a target for Browser Mod _services_.
|
|
||||||
|
|
||||||
### Browser ID
|
|
||||||
|
|
||||||
This box lets you set the `BrowserID` for the current _Browser_.
|
|
||||||
Note that it is possible to assign the same `BrowserID` to several browsers, but unpredictable things _may_ happen if several of them are open at the same time.
|
|
||||||
There may be benefits to using the same `BrowserID` in some cases, so you'll have to experiment with what works for you.
|
|
||||||
|
|
||||||
Browser Mod is trying hard to keep the Browser ID constant
|
|
||||||
|
|
||||||
### Enable camera
|
|
||||||
|
|
||||||
If your device has a camera, this will allow it to be forwarded as a `camera` entity to Home Assistant.
|
|
||||||
|
|
||||||
## Registered Browsers
|
|
||||||
|
|
||||||
This section shows all currently registered _Browsers_ and allows you to unregister them. This is useful e.g. if a `BrowserID` has changed or if you do not have access to a device anymore.
|
|
||||||
|
|
||||||
### Register CAST browser
|
|
||||||
|
|
||||||
If you are using [Home Assistant Cast](https://www.home-assistant.io/integrations/cast/#home-assistant-cast) to display a lovelace view on a Chromecast device it will get a BrowserID of "`CAST`". Since you can't access the Browser Mod config panel from the device, clicking this button will register the `CAST` browser. Most Browser Mod services will work under Home Assistant Cast.
|
|
||||||
|
|
||||||
## Frontend Settings
|
|
||||||
|
|
||||||
This section is for settings that change the default behavior of the Home Assistant frontend.
|
|
||||||
|
|
||||||
For each option the first applicable value will be applied.
|
|
||||||
|
|
||||||
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".
|
|
||||||

|
|
||||||
|
|
||||||
### 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
|
|
||||||
> ```
|
|
||||||
|
|
||||||
### Favicon template
|
|
||||||
|
|
||||||
This allows you to set and dynamically update the favicon of the browser tab/window. I.e. the little icon next to the page title. Favicons can be .png or .ico files and should be placed in your `<config>/www` directory. The box here should then contain a jinja [template](https://www.home-assistant.io/docs/configuration/templating/) which resolves to the path of the icon with `<config>/www/` replaced by `/local/` (see [Hosting files](https://www.home-assistant.io/integrations/http/#hosting-files)).
|
|
||||||
|
|
||||||
> Ex:
|
|
||||||
>
|
|
||||||
> ```jinja
|
|
||||||
> {% if is_state("light.bed_light", "on") %}
|
|
||||||
> /local/icons/green.png
|
|
||||||
> {% else %}
|
|
||||||
> /local/icons/red.png
|
|
||||||
> {% 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) custom integration.
|
|
||||||
|
|
||||||
|
|
||||||
### Hide Sidebar
|
|
||||||
|
|
||||||
This will hide the sidebar wit the navigation links. You can still access all the pages via normal links.
|
|
||||||
|
|
||||||
> Tip: add `/browser-mod` to the end of your home assistant URL when you need to turn this off again...
|
|
||||||
|
|
||||||
### Hide header
|
|
||||||
|
|
||||||
This will hide the header bar. Completely. It does not care if there are useful navigation links there or not. It's gone.
|
|
||||||
|
|
||||||
> 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.
|
|
||||||
|
|
||||||
### Default dashboard
|
|
||||||
|
|
||||||
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/).
|
|
@ -1,161 +0,0 @@
|
|||||||
|
|
||||||
## Anatomy of a popup
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
title: The title
|
|
||||||
content: The content
|
|
||||||
right_button: Right button
|
|
||||||
left_button: Left button
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Size
|
|
||||||
|
|
||||||
The `size` parameter can be set to `normal`, `wide` and `fullscreen` with results as below (background blur has been exagerated for clarity):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## HTML content
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
title: HTML content
|
|
||||||
content: |
|
|
||||||
An <b>HTML</b> string.
|
|
||||||
<p> Pretty much any HTML works: <ha-icon icon="mdi:lamp" style="color: red;"></ha-icon>
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Dashboard card content
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
title: HTML content
|
|
||||||
content:
|
|
||||||
type: entities
|
|
||||||
entities:
|
|
||||||
- light.bed_light
|
|
||||||
- light.ceiling_lights
|
|
||||||
- light.kitchen_lights
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Form content
|
|
||||||
`content` can be a list of ha-form schemas and the popup will then contain a form for user input:
|
|
||||||
|
|
||||||
```
|
|
||||||
<ha-form schema>:
|
|
||||||
name: <string>
|
|
||||||
[label: <string>]
|
|
||||||
[default: <any>]
|
|
||||||
selector: <Home Assistant Selector>
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|-|-|
|
|
||||||
| `name` | A unique parameter name |
|
|
||||||
| `label` | A description of the parameter |
|
|
||||||
| `default` | The default value for the parameter |
|
|
||||||
| `selector` | A [Home Assistant selector](https://www.home-assistant.io/docs/blueprint/selectors) |
|
|
||||||
|
|
||||||
The data from the form will be forwarded as data for any `right_button_action` or `left_button_action` of the popup.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
title: Form content
|
|
||||||
content:
|
|
||||||
- name: parameter_name
|
|
||||||
label: Descriptive name
|
|
||||||
selector:
|
|
||||||
text: null
|
|
||||||
- name: another_parameter
|
|
||||||
label: A number
|
|
||||||
default: 5
|
|
||||||
selector:
|
|
||||||
number:
|
|
||||||
min: 0
|
|
||||||
max: 10
|
|
||||||
slider: true
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Actionable popups
|
|
||||||
|
|
||||||
Example of a popup with actions opening more popups or calling Home Assistant services:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
content: Do you want to turn the light on?
|
|
||||||
right_button: "Yes"
|
|
||||||
left_button: "No"
|
|
||||||
right_button_action:
|
|
||||||
service: light.turn_on
|
|
||||||
data:
|
|
||||||
entity_id: light.bed_light
|
|
||||||
left_button_action:
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
title: Really?
|
|
||||||
content: Are you sure?
|
|
||||||
right_button: "Yes"
|
|
||||||
left_button: "No"
|
|
||||||
right_button_action:
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
content: Fine, live in darkness.
|
|
||||||
dismissable: false
|
|
||||||
title: Ok
|
|
||||||
timeout: 3000
|
|
||||||
left_button_action:
|
|
||||||
service: light.turn_on
|
|
||||||
data:
|
|
||||||
entity_id: light.bed_light
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Forward form data
|
|
||||||
|
|
||||||
The following popup would ask the user for a list of rooms to vacuum and then populate the `params` parameter of the `vacuum.send_command` service call from the result:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
title: Where to vacuum?
|
|
||||||
right_button: Go!
|
|
||||||
right_button_action:
|
|
||||||
service: vacuum.send_command
|
|
||||||
data:
|
|
||||||
entity_id: vacuum.xiaomi
|
|
||||||
command: app_segment_clean
|
|
||||||
content:
|
|
||||||
- name: params
|
|
||||||
label: Rooms to clean
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- label: Kitchen
|
|
||||||
value: 11
|
|
||||||
- label: Living room
|
|
||||||
value: 13
|
|
||||||
- label: Bedroom
|
|
||||||
value: 12
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
@ -1,288 +0,0 @@
|
|||||||
|
|
||||||
## Reading guide
|
|
||||||
Service parameters are described using the following conventions:
|
|
||||||
|
|
||||||
- `<type>` in brackets describe the type of a parameter, e.g.
|
|
||||||
|
|
||||||
- `<string>` is a piece of text
|
|
||||||
- `<number>` is a number
|
|
||||||
- `<TRUE/false>` means the value must be either `true` or `false` with `true` being the default
|
|
||||||
- `<service call>` means a full service call specification. Note that this can be any service, not just Browser Mod services
|
|
||||||
- `<Browser IDs>` is a list of BrowserIDs
|
|
||||||
|
|
||||||
- Square brackets `[ ]` indicate that a parameter is optional and can be omitted.
|
|
||||||
|
|
||||||
### `<service call>`
|
|
||||||
|
|
||||||
A service call is a combination of a service and it's data:
|
|
||||||
|
|
||||||
Ex, a `<service call>` for `browser_mod.more_info` with `light.bed_light` as entity:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.more_info
|
|
||||||
data:
|
|
||||||
entity: light.bed_light
|
|
||||||
```
|
|
||||||
|
|
||||||
If `data` contains `browser_id: THIS` then `THIS` will be replaced with the current browser ID.
|
|
||||||
|
|
||||||
|
|
||||||
## A note about targets
|
|
||||||
|
|
||||||
Browser Mod services can be called in two different ways which behave slightly differently.
|
|
||||||
|
|
||||||
The first way is as a *server* call. This is when the service is called from a script or automation, from the dev-services panel or from a dashboard `call-service` action.
|
|
||||||
|
|
||||||
The second way is as a *browser* call. This is when the service is called from a dashboard `fire-dom-event` action, as a part of a `browser_mod.sequence` call or as a `browser_mod.popup` `_action`.
|
|
||||||
|
|
||||||
The notable difference between the two is when no target (`browser_id`) is specified, in which case:
|
|
||||||
- A *server* call will perform the service on ALL REGISTERED BROWSERS.
|
|
||||||
- A *browser* call will perform the service on THE CURRENT BROWSER, i.e. the browser it was called from.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Finally, in *browser* calls, a parameter `browser_id` with the value `THIS` will be replaced with the current Browsers browser ID.
|
|
||||||
|
|
||||||
Ex:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
service: script.print_clicking_browser
|
|
||||||
data:
|
|
||||||
browser_id: THIS
|
|
||||||
```
|
|
||||||
|
|
||||||
with the script:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
script:
|
|
||||||
print_clicking_browser:
|
|
||||||
sequence:
|
|
||||||
- service: system_log.write
|
|
||||||
data:
|
|
||||||
message: "Button was clicked in {{browser_id}}"
|
|
||||||
```
|
|
||||||
|
|
||||||
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:
|
|
||||||

|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
> Note: Since `browser_id` is common for all services it is not explained further.
|
|
||||||
|
|
||||||
## `browser_mod.navigate`
|
|
||||||
|
|
||||||
Point the browser to the given Home Assistant path.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.navigate
|
|
||||||
data:
|
|
||||||
path: <string>
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`path` | A Home Assistant path. <br/>E.x. `/lovelace/`, `/my-dashboard/bedroom`, `/browser_mod/`, `/config/devices/device/20911cc5a63b1caafa2089618545eb8a`...|
|
|
||||||
|
|
||||||
## `browser_mod.refresh`
|
|
||||||
|
|
||||||
Reload the current page.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.refresh
|
|
||||||
data:
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
## `browser_mod.more_info`
|
|
||||||
|
|
||||||
Show a more-info dialog.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.more_info
|
|
||||||
data:
|
|
||||||
entity: <string>
|
|
||||||
[large: <true/FALSE>]
|
|
||||||
[ignore_popup_card: <true/FALSE>]
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`entity`| The entity whose more-info dialog to display. |
|
|
||||||
|`large`| If true, the dialog will be displayed wider, as if you had clicked the title of the dialog. |
|
|
||||||
| `ignore_popup_card` | If true the more-info dialog will be shown even if there's currently a popup-card which would override it. |
|
|
||||||
|
|
||||||
## `browser_mod.popup`
|
|
||||||
|
|
||||||
Display a popup dialog
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
[title: <string>]
|
|
||||||
content: <string / Dashboard card configuration / ha-form schema>
|
|
||||||
[size: <NORMAL/wide/fullscreen>]
|
|
||||||
[right_button: <string>]
|
|
||||||
[right_button_action: <service call>]
|
|
||||||
[left_button: <string>]
|
|
||||||
[left_button_action: <service call>]
|
|
||||||
[dismissable: <TRUE/false>]
|
|
||||||
[dismiss_action: <service call>]
|
|
||||||
[autoclose: <true/FALSE>]
|
|
||||||
[timeout: <number>]
|
|
||||||
[timeout_action: <service call>]
|
|
||||||
[style: <string>]
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`title` | The title of the popup window.|
|
|
||||||
|`content`| HTML, a dashboard card configuration or ha-form schema to display.|
|
|
||||||
| `size` | `wide` will make the popup window wider. `fullscreen` will make it cover the entire screen. |
|
|
||||||
| `right_button`| The text of the right action button.|
|
|
||||||
| `right_button_action`| Action to perform when the right action button is pressed. |
|
|
||||||
| `left_button`| The text of the left action button.|
|
|
||||||
| `left_button_action`| Action to perform when the left action button is pressed. |
|
|
||||||
| `dismissable`| If false the dialog cannot be closed by the user without clicking an action button. |
|
|
||||||
| `dismiss_action` | An action to perform if the dialog is closed by the user without clicking an action button. |
|
|
||||||
| `autoclose` | If true the dialog will close automatically when the mouse, screen or keyboard is touched. This will perform the `dismiss_action`. |
|
|
||||||
| `timeout` | If set will close the dialog after `timeout` milliseconds. |
|
|
||||||
| `timeout_action` | An action to perform if the dialog is closed by timeout. |
|
|
||||||
| `style` | CSS styles to apply to the dialog. |
|
|
||||||
|
|
||||||
The default value for `style` is as follows:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
style:
|
|
||||||
--popup-min-width: 400px;
|
|
||||||
--popup-max-width: 600px;
|
|
||||||
--popup-border-width: var(--ha-card-border-width, 2px);
|
|
||||||
--popup-border-color: var(--ha-card-border-color, var(--divider-color, #eee));
|
|
||||||
--popup-border-radius: 8px;
|
|
||||||
--popup-background-color: var(--ha-card-background, var(--card-background-color, white));
|
|
||||||
--popup-header-background-color: var(--popup-background-color, var(--sidebar-background-color));
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that any Browser Mod services performed as `_action`s here will be performed only on the same Browser as initiated the action unless `browser_id` is given.
|
|
||||||
|
|
||||||
If a ha-form schema is used for `content` the resulting data will be inserted into the `data` for any `_action`.
|
|
||||||
|
|
||||||
See [popups.md](popups.md) for more information and usage examples.
|
|
||||||
|
|
||||||
|
|
||||||
## `browser_mod.close_popup`
|
|
||||||
|
|
||||||
Close any currently open popup or more-info dialog.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.close_popup
|
|
||||||
data:
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
## `browser_mod.sequence`
|
|
||||||
|
|
||||||
Perform several services sequentially.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.sequence
|
|
||||||
data:
|
|
||||||
sequence:
|
|
||||||
- <service call>
|
|
||||||
- <service call>
|
|
||||||
- ...
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`sequence` | List of actions to perform. |
|
|
||||||
|
|
||||||
Note that if `browser_id` is omitted in the service calls listed in `sequence` the services will be performed on the Browser that's targeted as a whole rather than all browsers.
|
|
||||||
|
|
||||||
## `browser_mod.delay`
|
|
||||||
|
|
||||||
Wait for a specified time.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.delay
|
|
||||||
data:
|
|
||||||
time: <number>
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`time` | Number of milliseconds to wait.|
|
|
||||||
|
|
||||||
This is probably most useful as part of a `browser_mod.sequence` call.
|
|
||||||
|
|
||||||
## `browsermod.console`
|
|
||||||
|
|
||||||
Print a text to the browsers javascript console.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.console
|
|
||||||
data:
|
|
||||||
message: <string>
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`message` | Text to print. |
|
|
||||||
|
|
||||||
## `browsermod.javascript`
|
|
||||||
|
|
||||||
Run arbitrary javascript code in the browser.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service: browser_mod.console
|
|
||||||
data:
|
|
||||||
code: <string>
|
|
||||||
[browser_id: <Browser IDs>]
|
|
||||||
```
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
|`code` | Code to run. |
|
|
||||||
|
|
||||||
Only use this one if you know what you're doing.
|
|
||||||
|
|
||||||
The `hass` frontend object is available as global variable `hass`.
|
|
108
js/browser-player.js
Normal file
108
js/browser-player.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { LitElement, html, css } from "card-tools/src/lit-element";
|
||||||
|
import { deviceID } from "card-tools/src/deviceId"
|
||||||
|
import { moreInfo } from "card-tools/src/more-info"
|
||||||
|
|
||||||
|
class BrowserPlayer extends LitElement {
|
||||||
|
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
handleMute(ev) {
|
||||||
|
window.browser_mod.mute({});
|
||||||
|
}
|
||||||
|
handleVolumeChange(ev) {
|
||||||
|
const vol = parseFloat(ev.target.value);
|
||||||
|
window.browser_mod.set_volume({volume_level: vol});
|
||||||
|
}
|
||||||
|
handleMoreInfo(ev) {
|
||||||
|
moreInfo("media_player."+window.browser_mod.entity_id);
|
||||||
|
}
|
||||||
|
handlePlayPause(ev) {
|
||||||
|
if (window.browser_mod.player.paused)
|
||||||
|
window.browser_mod.play({});
|
||||||
|
else
|
||||||
|
window.browser_mod.pause({});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const player = window.browser_mod.player;
|
||||||
|
return html`
|
||||||
|
<ha-card>
|
||||||
|
<div class="card-content">
|
||||||
|
<paper-icon-button
|
||||||
|
.icon=${player.muted
|
||||||
|
? "mdi:volume-off"
|
||||||
|
: "mdi:volume-high"
|
||||||
|
}
|
||||||
|
@click=${this.handleMute}
|
||||||
|
></paper-icon-button>
|
||||||
|
<ha-paper-slider
|
||||||
|
min=0
|
||||||
|
max=1
|
||||||
|
step=0.01
|
||||||
|
?disabled=${player.muted}
|
||||||
|
value=${player.volume}
|
||||||
|
@change=${this.handleVolumeChange}
|
||||||
|
></ha-paper-slider>
|
||||||
|
|
||||||
|
${window.browser_mod.player_state === "stopped"
|
||||||
|
? html`<div class="placeholder"></div>`
|
||||||
|
: html`
|
||||||
|
<paper-icon-button
|
||||||
|
.icon=${player.paused
|
||||||
|
? "mdi:play"
|
||||||
|
: "mdi:pause"
|
||||||
|
}
|
||||||
|
@click=${this.handlePlayPause}
|
||||||
|
highlight
|
||||||
|
></paper-icon-button>
|
||||||
|
`}
|
||||||
|
<paper-icon-button
|
||||||
|
.icon=${"mdi:settings"}
|
||||||
|
@click=${this.handleMoreInfo}
|
||||||
|
></paper-icon-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="device-id">
|
||||||
|
${deviceID}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
paper-icon-button[highlight] {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
width: 24px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.device-id {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: xx-small;
|
||||||
|
margin-top: -10px;
|
||||||
|
user-select: all;
|
||||||
|
-webkit-user-select: all;
|
||||||
|
-moz-user-select: all;
|
||||||
|
-ms-user-select: all;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!customElements.get("browser-player"))
|
||||||
|
customElements.define("browser-player", BrowserPlayer);
|
@ -1,294 +0,0 @@
|
|||||||
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);
|
|
@ -1,261 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property, state } from "lit/decorators.js";
|
|
||||||
|
|
||||||
class BrowserModRegisteredBrowsersCard extends LitElement {
|
|
||||||
@property() hass;
|
|
||||||
@property() dirty = false;
|
|
||||||
|
|
||||||
toggleRegister() {
|
|
||||||
if (!window.browser_mod?.connected) return;
|
|
||||||
window.browser_mod.registered = !window.browser_mod.registered;
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
changeBrowserID(ev) {
|
|
||||||
window.browser_mod.browserID = ev.target.value;
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
toggleCameraEnabled() {
|
|
||||||
window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled;
|
|
||||||
this.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
firstUpdated() {
|
|
||||||
window.browser_mod.addEventListener("browser-mod-config-update", () =>
|
|
||||||
this.requestUpdate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<ha-card outlined>
|
|
||||||
<h1 class="card-header">
|
|
||||||
<div class="name">This Browser</div>
|
|
||||||
${window.browser_mod?.connected
|
|
||||||
? html`
|
|
||||||
<ha-icon
|
|
||||||
class="icon"
|
|
||||||
.icon=${"mdi:check-circle-outline"}
|
|
||||||
style="color: var(--success-color, green);"
|
|
||||||
></ha-icon>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-icon
|
|
||||||
class="icon"
|
|
||||||
.icon=${"mdi:circle-outline"}
|
|
||||||
style="color: var(--error-color, red);"
|
|
||||||
></ha-icon>
|
|
||||||
`}
|
|
||||||
</h1>
|
|
||||||
<div class="card-content">
|
|
||||||
${this.dirty
|
|
||||||
? html`
|
|
||||||
<ha-alert alert-type="warning">
|
|
||||||
It is strongly recommended to refresh your browser window
|
|
||||||
after changing any of the settings in this box.
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Register</span>
|
|
||||||
<span slot="description"
|
|
||||||
>Enable this browser as a Device in Home Assistant</span
|
|
||||||
>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${window.browser_mod?.registered}
|
|
||||||
@change=${this.toggleRegister}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-settings-row>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Browser ID</span>
|
|
||||||
<span slot="description"
|
|
||||||
>A unique identifier for this browser-device combination.</span
|
|
||||||
>
|
|
||||||
<ha-textfield
|
|
||||||
.value=${window.browser_mod?.browserID}
|
|
||||||
@change=${this.changeBrowserID}
|
|
||||||
></ha-textfield>
|
|
||||||
</ha-settings-row>
|
|
||||||
|
|
||||||
${window.browser_mod?.registered
|
|
||||||
? html`
|
|
||||||
${this._renderSuspensionAlert()}
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Enable camera</span>
|
|
||||||
<span slot="description"
|
|
||||||
>Get camera input from this browser (hardware
|
|
||||||
dependent)</span
|
|
||||||
>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${window.browser_mod?.cameraEnabled}
|
|
||||||
@change=${this.toggleCameraEnabled}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-settings-row>
|
|
||||||
${window.browser_mod?.cameraError
|
|
||||||
? html`
|
|
||||||
<ha-alert alert-type="error">
|
|
||||||
Setting up the device camera failed. Make sure you have
|
|
||||||
allowed use of the camera in your browser.
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this._renderInteractionAlert()}
|
|
||||||
${this._renderFKBSettingsInfo()}
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderSuspensionAlert() {
|
|
||||||
if (!this.hass.suspendWhenHidden) return html``;
|
|
||||||
return html`
|
|
||||||
<ha-alert alert-type="warning" title="Auto closing connection">
|
|
||||||
Home Assistant will close the websocket connection to the server
|
|
||||||
automatically after 5 minutes of inactivity.<br /><br />
|
|
||||||
While decreasing network trafic and memory usage, this may cause
|
|
||||||
problems for browser_mod operation.
|
|
||||||
<br /><br />
|
|
||||||
If you find that some things stop working for this Browser after a time,
|
|
||||||
try going to your
|
|
||||||
<a
|
|
||||||
href="/profile"
|
|
||||||
style="text-decoration: underline; color: var(--primary-color);"
|
|
||||||
>Profile Settings</a
|
|
||||||
>
|
|
||||||
and disabling the option
|
|
||||||
"${this.hass.localize("ui.panel.profile.suspend.header") ||
|
|
||||||
"Automatically close connection"}".
|
|
||||||
</ha-alert>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderInteractionAlert() {
|
|
||||||
return html`
|
|
||||||
<ha-alert title="Interaction requirement">
|
|
||||||
For privacy reasons many browsers require the user to interact with a
|
|
||||||
webpage before allowing audio playback or video capture. This may affect
|
|
||||||
the
|
|
||||||
<code>media_player</code> and <code>camera</code> components of Browser
|
|
||||||
Mod. <br /><br />
|
|
||||||
|
|
||||||
If you ever see a
|
|
||||||
<ha-icon icon="mdi:gesture-tap"></ha-icon> symbol at the bottom right
|
|
||||||
corner of the screen, please tap or click anywhere on the page. This
|
|
||||||
should allow Browser Mod to work again.
|
|
||||||
</ha-alert>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderFKBSettingsInfo() {
|
|
||||||
if (!window.browser_mod?.fully || !this.getFullySettings()) return html``;
|
|
||||||
return html`
|
|
||||||
${window.browser_mod?.fully && this.getFullySettings()
|
|
||||||
? html` <ha-alert title="FullyKiosk Browser">
|
|
||||||
You are using FullyKiosk Browser. It is recommended to enable the
|
|
||||||
following settings:
|
|
||||||
<ul>
|
|
||||||
${this.getFullySettings()}
|
|
||||||
</ul>
|
|
||||||
</ha-alert>`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFullySettings() {
|
|
||||||
if (!window.browser_mod.fully) return null;
|
|
||||||
const retval = [];
|
|
||||||
const wcs = [];
|
|
||||||
// Web Content Settings
|
|
||||||
// Autoplay Videos
|
|
||||||
if (window.fully.getBooleanSetting("autoplayVideos") !== "true")
|
|
||||||
wcs.push(html`<li>Autoplay Videos</li>`);
|
|
||||||
// Autoplay Audio
|
|
||||||
if (window.fully.getBooleanSetting("autoplayAudio") !== "true")
|
|
||||||
wcs.push(html`<li>Autoplay Audio</li>`);
|
|
||||||
// Enable Webcam Access (PLUS)
|
|
||||||
if (window.fully.getBooleanSetting("webcamAccess") !== "true")
|
|
||||||
wcs.push(html`<li>Enable Webcam Access (PLUS)</li>`);
|
|
||||||
|
|
||||||
if (wcs.length !== 0) {
|
|
||||||
retval.push(html`<li>Web Content Settings</li>
|
|
||||||
<ul>
|
|
||||||
${wcs}
|
|
||||||
</ul>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advanced Web Settings
|
|
||||||
// Enable JavaScript Interface (PLUS)
|
|
||||||
if (window.fully.getBooleanSetting("websiteIntegration") !== "true")
|
|
||||||
retval.push(html`<li>Advanced Web Settings</li>
|
|
||||||
<ul>
|
|
||||||
<li>Enable JavaScript Interface (PLUS)</li>
|
|
||||||
</ul>`);
|
|
||||||
|
|
||||||
// Device Management
|
|
||||||
// Keep Screen On
|
|
||||||
if (window.fully.getBooleanSetting("keepScreenOn") !== "true")
|
|
||||||
retval.push(html`<li>Device Management</li>
|
|
||||||
<ul>
|
|
||||||
<li>Keep Screen On</li>
|
|
||||||
</ul>`);
|
|
||||||
|
|
||||||
// Power Settings
|
|
||||||
// Prevent from Sleep while Screen Off
|
|
||||||
if (window.fully.getBooleanSetting("preventSleepWhileScreenOff") !== "true")
|
|
||||||
retval.push(html`<li>Power Settings</li>
|
|
||||||
<ul>
|
|
||||||
<li>Prevent from Sleep while Screen Off</li>
|
|
||||||
</ul>`);
|
|
||||||
|
|
||||||
const md = [];
|
|
||||||
// Motion Detection (PLUS)
|
|
||||||
// Enable Visual Motion Detection
|
|
||||||
if (window.fully.getBooleanSetting("motionDetection") !== "true")
|
|
||||||
md.push(html`<li>Enable Visual Motion Detection</li>`);
|
|
||||||
// Turn Screen On on Motion
|
|
||||||
if (window.fully.getBooleanSetting("screenOnOnMotion") !== "true")
|
|
||||||
md.push(html`<li>Turn Screen On on Motion</li>`);
|
|
||||||
// Exit Screensaver on Motion
|
|
||||||
if (window.fully.getBooleanSetting("stopScreensaverOnMotion") !== "true")
|
|
||||||
md.push(html`<li>Exit Screensaver on Motion</li>`);
|
|
||||||
|
|
||||||
if (md.length !== 0) {
|
|
||||||
retval.push(html`<li>Motion Detection (PLUS)</li>
|
|
||||||
<ul>
|
|
||||||
${md}
|
|
||||||
</ul>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote Administration (PLUS)
|
|
||||||
// Enable Remote Administration
|
|
||||||
if (window.fully.getBooleanSetting("remoteAdmin") !== "true")
|
|
||||||
retval.push(html`<li>Remote Administration (PLUS)</li>
|
|
||||||
<ul>
|
|
||||||
<li>Enable Remote Administration</li>
|
|
||||||
</ul>`);
|
|
||||||
|
|
||||||
return retval.length ? retval : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
ha-textfield {
|
|
||||||
width: 250px;
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define(
|
|
||||||
"browser-mod-browser-settings-card",
|
|
||||||
BrowserModRegisteredBrowsersCard
|
|
||||||
);
|
|
@ -1,221 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property, state } from "lit/decorators.js";
|
|
||||||
import { loadDeveloperToolsTemplate, selectTree } from "../helpers";
|
|
||||||
|
|
||||||
import "./browser-mod-settings-table";
|
|
||||||
|
|
||||||
loadDeveloperToolsTemplate();
|
|
||||||
|
|
||||||
class BrowserModFrontendSettingsCard extends LitElement {
|
|
||||||
@property() hass;
|
|
||||||
|
|
||||||
@state() _dashboards = [];
|
|
||||||
|
|
||||||
@state() _editSidebar = false;
|
|
||||||
_savedSidebar = { panelOrder: [], hiddenPanels: [] };
|
|
||||||
|
|
||||||
firstUpdated() {
|
|
||||||
window.browser_mod.addEventListener("browser-mod-config-update", () =>
|
|
||||||
this.requestUpdate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(changedProperties) {
|
|
||||||
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() {
|
|
||||||
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`
|
|
||||||
<ha-card header="Frontend Settings" outlined>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-alert alert-type="warning" title="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
|
|
||||||
you made a setting here when you switch devices or user.
|
|
||||||
<p>
|
|
||||||
Do not report any issues to Home Assistant before clearing
|
|
||||||
<b>EVERY</b> setting here and thouroghly clearing all your browser
|
|
||||||
caches. Failure to do so means you risk wasting a lot of peoples
|
|
||||||
time, and you will be severly and rightfully ridiculed.
|
|
||||||
</p>
|
|
||||||
</ha-alert>
|
|
||||||
<p>
|
|
||||||
Settings below are applied by first match. I.e. if a matching User
|
|
||||||
setting exists, it will be applied. Otherwise any matching Browser
|
|
||||||
setting and otherwise the GLOBAL setting if that differs from
|
|
||||||
DEFAULT.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Title template</span>
|
|
||||||
<span slot="description">
|
|
||||||
Jinja template for the browser window/tab title
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
|
||||||
<browser-mod-settings-table
|
|
||||||
.hass=${this.hass}
|
|
||||||
.settingKey=${"titleTemplate"}
|
|
||||||
></browser-mod-settings-table>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Favicon template</span>
|
|
||||||
<span slot="description">
|
|
||||||
Jinja template for the browser favicon
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
|
||||||
<browser-mod-settings-table
|
|
||||||
.hass=${this.hass}
|
|
||||||
.settingKey=${"faviconTemplate"}
|
|
||||||
></browser-mod-settings-table>
|
|
||||||
|
|
||||||
<div class="separator"></div>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Hide sidebar</span>
|
|
||||||
<span slot="description">
|
|
||||||
Completely remove the sidebar from all panels
|
|
||||||
</span>
|
|
||||||
</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>
|
|
||||||
<span slot="heading">Hide header</span>
|
|
||||||
<span slot="description">
|
|
||||||
Completely remove the header from all panels
|
|
||||||
</span>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Sidebar order</span>
|
|
||||||
<span slot="description">
|
|
||||||
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>
|
|
||||||
<mwc-button @click=${() => this.toggleEditSidebar()}>
|
|
||||||
${this._editSidebar ? "Restore" : "Edit"}
|
|
||||||
</mwc-button>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">Sidebar title</span>
|
|
||||||
<span slot="description">
|
|
||||||
The title at the top of the sidebar
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
|
||||||
<browser-mod-settings-table
|
|
||||||
.hass=${this.hass}
|
|
||||||
.settingKey=${"sidebarTitle"}
|
|
||||||
.settingSelector=${{ text: {} }}
|
|
||||||
></browser-mod-settings-table>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
.box {
|
|
||||||
border: 1px solid var(--divider-color);
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
.separator {
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
margin: 16px -16px 0px;
|
|
||||||
}
|
|
||||||
img.favicon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
mwc-tab-bar ha-icon {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define(
|
|
||||||
"browser-mod-frontend-settings-card",
|
|
||||||
BrowserModFrontendSettingsCard
|
|
||||||
);
|
|
@ -1,73 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property } from "lit/decorators.js";
|
|
||||||
import { loadConfigDashboard } from "../helpers";
|
|
||||||
|
|
||||||
import "./browser-settings-card";
|
|
||||||
import "./registered-browsers-card";
|
|
||||||
import "./frontend-settings-card";
|
|
||||||
|
|
||||||
const bmWindow = window as any;
|
|
||||||
|
|
||||||
loadConfigDashboard().then(() => {
|
|
||||||
class BrowserModPanel extends LitElement {
|
|
||||||
@property() hass;
|
|
||||||
@property() narrow;
|
|
||||||
@property() connection;
|
|
||||||
|
|
||||||
firstUpdated() {
|
|
||||||
window.addEventListener("browser-mod-config-update", () =>
|
|
||||||
this.requestUpdate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!window.browser_mod) return html``;
|
|
||||||
return html`
|
|
||||||
<ha-app-layout>
|
|
||||||
<app-header slot="header" fixed>
|
|
||||||
<app-toolbar>
|
|
||||||
<ha-menu-button
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
></ha-menu-button>
|
|
||||||
<div main-title>Browser Mod Settings</div>
|
|
||||||
</app-toolbar>
|
|
||||||
</app-header>
|
|
||||||
|
|
||||||
<ha-config-section .narrow=${this.narrow} full-width>
|
|
||||||
<browser-mod-browser-settings-card
|
|
||||||
.hass=${this.hass}
|
|
||||||
></browser-mod-browser-settings-card>
|
|
||||||
|
|
||||||
<browser-mod-registered-browsers-card
|
|
||||||
.hass=${this.hass}
|
|
||||||
></browser-mod-registered-browsers-card>
|
|
||||||
|
|
||||||
<browser-mod-frontend-settings-card
|
|
||||||
.hass=${this.hass}
|
|
||||||
></browser-mod-frontend-settings-card>
|
|
||||||
</ha-config-section>
|
|
||||||
</ha-app-layout>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return [
|
|
||||||
...((customElements.get("ha-config-dashboard") as any)?.styles ?? []),
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
--app-header-background-color: var(--sidebar-background-color);
|
|
||||||
--app-header-text-color: var(--sidebar-text-color);
|
|
||||||
--app-header-border-bottom: 1px solid var(--divider-color);
|
|
||||||
--ha-card-border-radius: var(--ha-config-card-border-radius, 8px);
|
|
||||||
}
|
|
||||||
ha-config-section {
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("browser-mod-panel", BrowserModPanel);
|
|
||||||
});
|
|
@ -1,100 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property, state } from "lit/decorators.js";
|
|
||||||
|
|
||||||
class BrowserModRegisteredBrowsersCard extends LitElement {
|
|
||||||
@property() hass;
|
|
||||||
|
|
||||||
firstUpdated() {
|
|
||||||
window.browser_mod.addEventListener("browser-mod-config-update", () =>
|
|
||||||
this.requestUpdate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister_browser(ev) {
|
|
||||||
const browserID = ev.currentTarget.browserID;
|
|
||||||
|
|
||||||
const unregisterCallback = () => {
|
|
||||||
if (browserID === window.browser_mod.browserID) {
|
|
||||||
window.browser_mod.registered = false;
|
|
||||||
} else {
|
|
||||||
window.browser_mod.connection.sendMessage({
|
|
||||||
type: "browser_mod/unregister",
|
|
||||||
browserID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.browser_mod.showPopup(
|
|
||||||
"Unregister browser",
|
|
||||||
`Are you sure you want to unregister Browser ${browserID}?`,
|
|
||||||
{
|
|
||||||
right_button: "Yes",
|
|
||||||
right_button_action: unregisterCallback,
|
|
||||||
left_button: "No",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
register_cast() {
|
|
||||||
window.browser_mod.connection.sendMessage({
|
|
||||||
type: "browser_mod/register",
|
|
||||||
browserID: "CAST",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<ha-card header="Registered Browsers" outlined>
|
|
||||||
<div class="card-content">
|
|
||||||
${Object.keys(window.browser_mod.browsers).map((d) => {
|
|
||||||
const browser = window.browser_mod.browsers[d];
|
|
||||||
return html` <ha-settings-row>
|
|
||||||
<span slot="heading"> ${d} </span>
|
|
||||||
<span slot="description">
|
|
||||||
Last connected:
|
|
||||||
<ha-relative-time
|
|
||||||
.hass=${this.hass}
|
|
||||||
.datetime=${browser.last_seen}
|
|
||||||
></ha-relative-time>
|
|
||||||
</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 .icon=${"mdi:delete"}></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
</ha-settings-row>`;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
${window.browser_mod.browsers["CAST"] === undefined
|
|
||||||
? html`
|
|
||||||
<div class="card-actions">
|
|
||||||
<mwc-button @click=${this.register_cast}>
|
|
||||||
Register CAST Browser
|
|
||||||
</mwc-button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
ha-icon-button > * {
|
|
||||||
display: flex;
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customElements.define(
|
|
||||||
"browser-mod-registered-browsers-card",
|
|
||||||
BrowserModRegisteredBrowsersCard
|
|
||||||
);
|
|
172
js/helpers.ts
172
js/helpers.ts
@ -1,172 +0,0 @@
|
|||||||
const TIMEOUT_ERROR = "SELECTTREE-TIMEOUT";
|
|
||||||
|
|
||||||
export async function await_element(el, hard = false) {
|
|
||||||
if (el.localName?.includes("-"))
|
|
||||||
await customElements.whenDefined(el.localName);
|
|
||||||
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) {
|
|
||||||
let el = [root];
|
|
||||||
if (typeof path === "string") {
|
|
||||||
path = path.split(/(\$| )/);
|
|
||||||
}
|
|
||||||
while (path[path.length - 1] === "") path.pop();
|
|
||||||
for (const [i, p] of path.entries()) {
|
|
||||||
const e = el[0];
|
|
||||||
if (!e) return null;
|
|
||||||
|
|
||||||
if (!p.trim().length) continue;
|
|
||||||
|
|
||||||
await_element(e);
|
|
||||||
el = p === "$" ? [e.shadowRoot] : e.querySelectorAll(p);
|
|
||||||
}
|
|
||||||
return all ? el : el[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function selectTree(root, path, all = false, timeout = 10000) {
|
|
||||||
return Promise.race([
|
|
||||||
_selectTree(root, path, all),
|
|
||||||
new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error(TIMEOUT_ERROR)), timeout)
|
|
||||||
),
|
|
||||||
]).catch((err) => {
|
|
||||||
if (!err.message || err.message !== TIMEOUT_ERROR) throw err;
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function hass_base_el() {
|
|
||||||
await Promise.race([
|
|
||||||
customElements.whenDefined("home-assistant"),
|
|
||||||
customElements.whenDefined("hc-main"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const element = customElements.get("home-assistant")
|
|
||||||
? "home-assistant"
|
|
||||||
: "hc-main";
|
|
||||||
|
|
||||||
while (!document.querySelector(element))
|
|
||||||
await new Promise((r) => window.setTimeout(r, 100));
|
|
||||||
return document.querySelector(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function hass() {
|
|
||||||
const base: any = await hass_base_el();
|
|
||||||
while (!base.hass) await new Promise((r) => window.setTimeout(r, 100));
|
|
||||||
return base.hass;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function provideHass(el) {
|
|
||||||
const base: any = await hass_base_el();
|
|
||||||
base.provideHass(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadLoadCardHelpers = async () => {
|
|
||||||
if (window.loadCardHelpers !== undefined) return;
|
|
||||||
|
|
||||||
await customElements.whenDefined("partial-panel-resolver");
|
|
||||||
const ppResolver = document.createElement("partial-panel-resolver");
|
|
||||||
const routes = (ppResolver as any).getRoutes([
|
|
||||||
{
|
|
||||||
component_name: "lovelace",
|
|
||||||
url_path: "a",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await routes?.routes?.a?.load?.();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadHaForm = async () => {
|
|
||||||
if (customElements.get("ha-form")) return;
|
|
||||||
await loadLoadCardHelpers();
|
|
||||||
const helpers = await window.loadCardHelpers();
|
|
||||||
if (!helpers) return;
|
|
||||||
const card = await helpers.createCardElement({ type: "button" });
|
|
||||||
if (!card) return;
|
|
||||||
await card.constructor.getConfigElement();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Loads in ha-config-dashboard which is used to copy styling
|
|
||||||
// Also provides ha-settings-row
|
|
||||||
export const loadConfigDashboard = async () => {
|
|
||||||
await customElements.whenDefined("partial-panel-resolver");
|
|
||||||
const ppResolver = document.createElement("partial-panel-resolver");
|
|
||||||
const routes = (ppResolver as any).getRoutes([
|
|
||||||
{
|
|
||||||
component_name: "config",
|
|
||||||
url_path: "a",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await routes?.routes?.a?.load?.();
|
|
||||||
await customElements.whenDefined("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?.cloud?.load?.(); // Load ha-settings-row
|
|
||||||
await configRouter?.routerOptions?.routes?.entities?.load?.(); // Load ha-data-table
|
|
||||||
await customElements.whenDefined("ha-config-dashboard");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadDeveloperToolsTemplate = async () => {
|
|
||||||
await customElements.whenDefined("partial-panel-resolver");
|
|
||||||
await customElements.whenDefined("partial-panel-resolver");
|
|
||||||
const ppResolver = document.createElement("partial-panel-resolver");
|
|
||||||
const routes = (ppResolver as any).getRoutes([
|
|
||||||
{
|
|
||||||
component_name: "developer-tools",
|
|
||||||
url_path: "a",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
await routes?.routes?.a?.load?.();
|
|
||||||
const dtRouter: any = document.createElement("developer-tools-router");
|
|
||||||
await dtRouter?.routerOptions?.routes?.template?.load?.();
|
|
||||||
await customElements.whenDefined("developer-tools-template");
|
|
||||||
};
|
|
||||||
|
|
||||||
export function throttle(timeout) {
|
|
||||||
return function (target, propertyKey, descriptor) {
|
|
||||||
const fn = descriptor.value;
|
|
||||||
let cooldown = undefined;
|
|
||||||
descriptor.value = function (...rest) {
|
|
||||||
if (cooldown) return;
|
|
||||||
cooldown = setTimeout(() => (cooldown = undefined), timeout);
|
|
||||||
return fn.bind(this)(...rest);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
435
js/main.js
Normal file
435
js/main.js
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
import { deviceID } from "card-tools/src/deviceId";
|
||||||
|
import { lovelace_view, provideHass, load_lovelace, lovelace, hass } from "card-tools/src/hass";
|
||||||
|
import { popUp, closePopUp } from "card-tools/src/popup";
|
||||||
|
import { fireEvent } from "card-tools/src/event";
|
||||||
|
import { moreInfo } from "card-tools/src/more-info.js";
|
||||||
|
import "./browser-player";
|
||||||
|
|
||||||
|
class BrowserMod {
|
||||||
|
|
||||||
|
set hass(hass) {
|
||||||
|
if(!hass) return;
|
||||||
|
this._hass = hass;
|
||||||
|
if(this.hassPatched) return;
|
||||||
|
const callService = hass.callService;
|
||||||
|
const newCallService = (domain, service, serviceData) => {
|
||||||
|
if(serviceData && serviceData.deviceID) {
|
||||||
|
if(Array.isArray(serviceData.deviceID)) {
|
||||||
|
const index = serviceData.deviceID.indexOf('this');
|
||||||
|
if(index !== -1)
|
||||||
|
serviceData.deviceID[index] = deviceID;
|
||||||
|
} else if(serviceData.deviceID === "this") {
|
||||||
|
serviceData.deviceID = deviceID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callService(domain, service, serviceData);
|
||||||
|
};
|
||||||
|
hass.callService = newCallService;
|
||||||
|
|
||||||
|
this.hassPatched = true;
|
||||||
|
if(document.querySelector("hc-main"))
|
||||||
|
document.querySelector("hc-main").hassChanged(hass,hass);
|
||||||
|
else
|
||||||
|
document.querySelector("home-assistant").hassChanged(hass, hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
playOnce(ev) {
|
||||||
|
if(this._video) this._video.play();
|
||||||
|
if(window.browser_mod.playedOnce) return;
|
||||||
|
window.browser_mod.player.play();
|
||||||
|
window.browser_mod.playedOnce = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_load_lovelace() {
|
||||||
|
if(!load_lovelace()) {
|
||||||
|
let timer = window.setTimeout(this._load_lovelace.bind(this), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.entity_id = deviceID.replace("-","_");
|
||||||
|
this.cast = document.querySelector("hc-main") !== null;
|
||||||
|
if(!this.cast) {
|
||||||
|
window.setTimeout(this._load_lovelace.bind(this), 500);
|
||||||
|
window.hassConnection.then((conn) => this.connect(conn.conn));
|
||||||
|
document.querySelector("home-assistant").addEventListener("hass-more-info", this.popup_card.bind(this));
|
||||||
|
} else {
|
||||||
|
this.connect(hass().connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player = new Audio();
|
||||||
|
this.playedOnce = false;
|
||||||
|
|
||||||
|
this.autoclose_popup_active = false;
|
||||||
|
|
||||||
|
const updater = this.update.bind(this);
|
||||||
|
this.player.addEventListener("ended", updater);
|
||||||
|
this.player.addEventListener("play", updater);
|
||||||
|
this.player.addEventListener("pause", updater);
|
||||||
|
this.player.addEventListener("volumechange", updater);
|
||||||
|
document.addEventListener("visibilitychange", updater);
|
||||||
|
window.addEventListener("location-changed", updater);
|
||||||
|
window.addEventListener("click", this.playOnce);
|
||||||
|
window.addEventListener("mousemove", this.no_blackout.bind(this));
|
||||||
|
window.addEventListener("mousedown", this.no_blackout.bind(this));
|
||||||
|
window.addEventListener("keydown", this.no_blackout.bind(this));
|
||||||
|
window.addEventListener("touchstart", this.no_blackout.bind(this));
|
||||||
|
provideHass(this);
|
||||||
|
|
||||||
|
if(window.fully)
|
||||||
|
{
|
||||||
|
this._fullyMotion = false;
|
||||||
|
this._motionTimeout = undefined;
|
||||||
|
fully.bind('screenOn', 'browser_mod.update();');
|
||||||
|
fully.bind('screenOff', 'browser_mod.update();');
|
||||||
|
fully.bind('pluggedAC', 'browser_mod.update();');
|
||||||
|
fully.bind('pluggedUSB', 'browser_mod.update();');
|
||||||
|
fully.bind('onBatteryLevelChanged', 'browser_mod.update();');
|
||||||
|
fully.bind('unplugged', 'browser_mod.update();');
|
||||||
|
fully.bind('networkReconnect', 'browser_mod.update();');
|
||||||
|
|
||||||
|
fully.bind('onMotion', 'browser_mod.fullyMotion();');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._screenSaver = undefined;
|
||||||
|
this._screenSaverTimer = undefined;
|
||||||
|
this._screenSaverTime = 0;
|
||||||
|
this._blackout = document.createElement("div");
|
||||||
|
this._blackout.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: black;
|
||||||
|
visibility: hidden;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this._blackout);
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(conn) {
|
||||||
|
this.conn = conn
|
||||||
|
conn.subscribeMessage((msg) => this.callback(msg), {
|
||||||
|
type: 'browser_mod/connect',
|
||||||
|
deviceID: deviceID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(msg) {
|
||||||
|
switch (msg.command) {
|
||||||
|
case "update":
|
||||||
|
this.update(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "debug":
|
||||||
|
this.debug(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "play":
|
||||||
|
this.play(msg);
|
||||||
|
break;
|
||||||
|
case "pause":
|
||||||
|
this.pause(msg);
|
||||||
|
break;
|
||||||
|
case "stop":
|
||||||
|
this.stop(msg);
|
||||||
|
break;
|
||||||
|
case "set_volume":
|
||||||
|
this.set_volume(msg);
|
||||||
|
break;
|
||||||
|
case "mute":
|
||||||
|
this.mute(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "toast":
|
||||||
|
this.toast(msg);
|
||||||
|
break;
|
||||||
|
case "popup":
|
||||||
|
this.popup(msg);
|
||||||
|
break;
|
||||||
|
case "close-popup":
|
||||||
|
this.close_popup(msg);
|
||||||
|
break;
|
||||||
|
case "navigate":
|
||||||
|
this.navigate(msg);
|
||||||
|
break;
|
||||||
|
case "more-info":
|
||||||
|
this.more_info(msg);
|
||||||
|
break;
|
||||||
|
case "set-theme":
|
||||||
|
this.set_theme(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "lovelace-reload":
|
||||||
|
this.lovelace_reload(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "blackout":
|
||||||
|
this.blackout(msg);
|
||||||
|
break;
|
||||||
|
case "no-blackout":
|
||||||
|
this.no_blackout(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get player_state() {
|
||||||
|
if (!this.player.src) return "stopped";
|
||||||
|
if (this.player.ended) return "stopped";
|
||||||
|
if (this.player.paused) return "paused";
|
||||||
|
return "playing";
|
||||||
|
}
|
||||||
|
|
||||||
|
popup_card(ev) {
|
||||||
|
const moreInfoEl = document.querySelector("home-assistant")._moreInfoEl;
|
||||||
|
if(moreInfoEl && !moreInfoEl.getAttribute('aria-hidden')) return;
|
||||||
|
if(!lovelace()) return;
|
||||||
|
const ll = lovelace();
|
||||||
|
const data = {
|
||||||
|
...ll.config.popup_cards,
|
||||||
|
...ll.config.views[ll.current_view].popup_cards,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!ev.detail || !ev.detail.entityId) return;
|
||||||
|
const d = data[ev.detail.entityId];
|
||||||
|
if(!d) return;
|
||||||
|
popUp(d.title, d.card, d.large || false, d.style);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(msg) {
|
||||||
|
popUp(`deviceID`, {type: "markdown", content: `# ${deviceID}`})
|
||||||
|
alert(deviceID);
|
||||||
|
}
|
||||||
|
|
||||||
|
_set_screensaver(fn, time) {
|
||||||
|
clearTimeout(this._screenSaverTimer);
|
||||||
|
if(!fn) {
|
||||||
|
if(this._screenSaverTime)
|
||||||
|
this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime)
|
||||||
|
} else {
|
||||||
|
time = parseInt(time)
|
||||||
|
if(time == -1) {
|
||||||
|
clearTimeout(this._screenSaverTimer);
|
||||||
|
this._screenSaverTime = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._screenSaverTime = time * 1000;
|
||||||
|
this._screenSaver = fn;
|
||||||
|
this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
play(msg) {
|
||||||
|
const src = msg.media_content_id;
|
||||||
|
if(src)
|
||||||
|
this.player.src = src;
|
||||||
|
this.player.play();
|
||||||
|
}
|
||||||
|
pause(msg) {
|
||||||
|
this.player.pause();
|
||||||
|
}
|
||||||
|
stop(msg) {
|
||||||
|
this.player.pause();
|
||||||
|
this.player.src = null;
|
||||||
|
}
|
||||||
|
set_volume(msg) {
|
||||||
|
if (msg.volume_level === undefined) return;
|
||||||
|
this.player.volume = msg.volume_level;
|
||||||
|
}
|
||||||
|
mute(msg) {
|
||||||
|
if (msg.mute === undefined)
|
||||||
|
msg.mute = !this.player.muted;
|
||||||
|
this.player.muted = Boolean(msg.mute)
|
||||||
|
}
|
||||||
|
|
||||||
|
toast(msg) {
|
||||||
|
if(!msg.message) return;
|
||||||
|
|
||||||
|
fireEvent("hass-notification", {
|
||||||
|
message: msg.message,
|
||||||
|
duration: msg.duration !== undefined ? parseInt(msg.duration) : undefined
|
||||||
|
}, document.querySelector("home-assistant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
popup(msg){
|
||||||
|
if(!msg.title && !msg.auto_close) return;
|
||||||
|
if(!msg.card) return;
|
||||||
|
|
||||||
|
const fn = () => {
|
||||||
|
popUp(msg.title, msg.card, msg.large, msg.style, msg.auto_close);
|
||||||
|
if(msg.auto_close)
|
||||||
|
this.autoclose_popup_active = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(msg.auto_close && msg.time) {
|
||||||
|
this._set_screensaver(fn, msg.time);
|
||||||
|
} else {
|
||||||
|
// closePopUp();
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close_popup(msg){
|
||||||
|
this._set_screensaver();
|
||||||
|
this.autoclose_popup_active = false;
|
||||||
|
closePopUp();
|
||||||
|
}
|
||||||
|
navigate(msg){
|
||||||
|
if(!msg.navigation_path) return;
|
||||||
|
history.pushState(null, "", msg.navigation_path);
|
||||||
|
fireEvent("location-changed", {}, document.querySelector("home-assistant"));
|
||||||
|
}
|
||||||
|
more_info(msg){
|
||||||
|
if(!msg.entity_id) return;
|
||||||
|
moreInfo(msg.entity_id, msg.large);
|
||||||
|
}
|
||||||
|
set_theme(msg){
|
||||||
|
if(!msg.theme) msg.theme = "default";
|
||||||
|
fireEvent("settheme", msg.theme, document.querySelector("home-assistant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
lovelace_reload(msg) {
|
||||||
|
const ll = lovelace_view();
|
||||||
|
if (ll)
|
||||||
|
fireEvent("config-refresh", {}, ll);
|
||||||
|
}
|
||||||
|
|
||||||
|
blackout(msg){
|
||||||
|
const fn = () => {
|
||||||
|
if (window.fully)
|
||||||
|
{
|
||||||
|
fully.turnScreenOff();
|
||||||
|
} else {
|
||||||
|
this._blackout.style.visibility = "visible";
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
};
|
||||||
|
if(msg.time) {
|
||||||
|
this._set_screensaver(fn, msg.time)
|
||||||
|
} else {
|
||||||
|
fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
no_blackout(msg){
|
||||||
|
this._set_screensaver();
|
||||||
|
if(this.autoclose_popup_active)
|
||||||
|
return this.close_popup();
|
||||||
|
if (window.fully)
|
||||||
|
{
|
||||||
|
if (!fully.getScreenOn())
|
||||||
|
fully.turnScreenOn();
|
||||||
|
if (msg.brightness)
|
||||||
|
fully.setScreenBrightness(msg.brightness);
|
||||||
|
this.update();
|
||||||
|
} else {
|
||||||
|
if(this._blackout.style.visibility !== "hidden") {
|
||||||
|
this._blackout.style.visibility = "hidden";
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is_blackout(){
|
||||||
|
if (window.fully)
|
||||||
|
return !fully.getScreenOn();
|
||||||
|
return Boolean(this._blackout.style.visibility === "visible")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullyMotion() {
|
||||||
|
this._fullyMotion = true;
|
||||||
|
clearTimeout(this._motionTimeout);
|
||||||
|
this._motionTimeout = setTimeout(() => {
|
||||||
|
this._fullyMotion = false;
|
||||||
|
this.update();
|
||||||
|
}, 5000);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start_camera() {
|
||||||
|
if(this._video) return;
|
||||||
|
this._video = document.createElement("video");
|
||||||
|
this._video.autoplay = true;
|
||||||
|
this._video.playsInline = true;
|
||||||
|
this._video.style.cssText = `
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
`;
|
||||||
|
this._canvas = document.createElement("canvas");
|
||||||
|
this._canvas.style.cssText = `
|
||||||
|
visibility: hidden;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(this._canvas);
|
||||||
|
document.body.appendChild(this._video);
|
||||||
|
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then((stream) => {
|
||||||
|
this._video.srcObject = stream;
|
||||||
|
this._video.play();
|
||||||
|
this.send_cam();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send_cam(data) {
|
||||||
|
const context = this._canvas.getContext('2d');
|
||||||
|
context.drawImage(this._video, 0, 0, this._canvas.width, this._canvas.height);
|
||||||
|
this.conn.sendMessage({
|
||||||
|
type: 'browser_mod/update',
|
||||||
|
deviceID: deviceID,
|
||||||
|
data: {
|
||||||
|
camera: this._canvas.toDataURL('image/png'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setTimeout(this.send_cam.bind(this), 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
update(msg=null) {
|
||||||
|
if(!this.conn) return;
|
||||||
|
|
||||||
|
if(msg) {
|
||||||
|
if(msg.name) {
|
||||||
|
this.entity_id = msg.name.toLowerCase();
|
||||||
|
}
|
||||||
|
if(msg.camera) {
|
||||||
|
this.start_camera();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conn.sendMessage({
|
||||||
|
type: 'browser_mod/update',
|
||||||
|
deviceID: deviceID,
|
||||||
|
data: {
|
||||||
|
browser: {
|
||||||
|
path: window.location.pathname,
|
||||||
|
visibility: document.visibilityState,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
currentUser: this._hass && this._hass.user && this._hass.user.name,
|
||||||
|
fullyKiosk: window.fully ? true : undefined,
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
},
|
||||||
|
player: {
|
||||||
|
volume: this.player.volume,
|
||||||
|
muted: this.player.muted,
|
||||||
|
src: this.player.src,
|
||||||
|
state: this.player_state,
|
||||||
|
},
|
||||||
|
screen: {
|
||||||
|
blackout: this.is_blackout(),
|
||||||
|
brightness: window.fully ? fully.getScreenBrightness() : undefined,
|
||||||
|
},
|
||||||
|
fully: window.fully ? {
|
||||||
|
battery: window.fully ? fully.getBatteryLevel() : undefined,
|
||||||
|
charging: window.fully ? fully.isPlugged(): undefined,
|
||||||
|
motion: window.fully ? this._fullyMotion : undefined,
|
||||||
|
} : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
window.browser_mod = window.browser_mod || new BrowserMod();
|
@ -1,43 +0,0 @@
|
|||||||
export const ActivityMixin = (SuperClass) => {
|
|
||||||
return class ActivityMixinClass extends SuperClass {
|
|
||||||
activityTriggered = false;
|
|
||||||
_activityCooldown = 15000;
|
|
||||||
_activityTimeout;
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
for (const ev of ["pointerdown", "pointermove", "keydown"]) {
|
|
||||||
window.addEventListener(ev, () => this.activityTrigger(true));
|
|
||||||
}
|
|
||||||
this.addEventListener("fully-update", () => {
|
|
||||||
this.activityTrigger();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
activityTrigger(touched = false) {
|
|
||||||
if (!this.activityTriggered) {
|
|
||||||
this.sendUpdate({
|
|
||||||
activity: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.activityTriggered = true;
|
|
||||||
if (touched) {
|
|
||||||
this.fireEvent("browser-mod-activity");
|
|
||||||
}
|
|
||||||
clearTimeout(this._activityTimeout);
|
|
||||||
this._activityTimeout = setTimeout(
|
|
||||||
() => this.activityReset(),
|
|
||||||
this._activityCooldown
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
activityReset() {
|
|
||||||
clearTimeout(this._activityTimeout);
|
|
||||||
if (this.activityTriggered) {
|
|
||||||
this.sendUpdate({
|
|
||||||
activity: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.activityTriggered = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,25 +0,0 @@
|
|||||||
import { LitElement, html } from "lit";
|
|
||||||
|
|
||||||
class BrowserPlayerEditor extends LitElement {
|
|
||||||
setConfig(config) {}
|
|
||||||
render() {
|
|
||||||
return html` <div>Nothing to configure.</div> `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (async () => {
|
|
||||||
// while (!window.browser_mod) {
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
// }
|
|
||||||
// await window.browser_mod.connectionPromise;
|
|
||||||
|
|
||||||
if (!customElements.get("browser-player-editor")) {
|
|
||||||
customElements.define("browser-player-editor", BrowserPlayerEditor);
|
|
||||||
window.customCards = window.customCards || [];
|
|
||||||
window.customCards.push({
|
|
||||||
type: "browser-player",
|
|
||||||
name: "Browser Player",
|
|
||||||
preview: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// })();
|
|
@ -1,179 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property } from "lit/decorators.js";
|
|
||||||
|
|
||||||
import "./browser-player-editor.ts";
|
|
||||||
|
|
||||||
import "./types";
|
|
||||||
|
|
||||||
class BrowserPlayer extends LitElement {
|
|
||||||
@property() hass;
|
|
||||||
@property({ attribute: "edit-mode", reflect: true }) editMode;
|
|
||||||
|
|
||||||
static getConfigElement() {
|
|
||||||
return document.createElement("browser-player-editor");
|
|
||||||
}
|
|
||||||
static getStubConfig() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
if (!window.browser_mod?.registered) {
|
|
||||||
if (this.parentElement.localName === "hui-card-preview") {
|
|
||||||
this.removeAttribute("hidden");
|
|
||||||
} else {
|
|
||||||
this.setAttribute("hidden", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async setConfig(config) {
|
|
||||||
while (!window.browser_mod) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const event of [
|
|
||||||
"play",
|
|
||||||
"pause",
|
|
||||||
"ended",
|
|
||||||
"volumechange",
|
|
||||||
"canplay",
|
|
||||||
"loadeddata",
|
|
||||||
])
|
|
||||||
window.browser_mod?._audio_player?.addEventListener(event, () =>
|
|
||||||
this.requestUpdate()
|
|
||||||
);
|
|
||||||
window.browser_mod?._video_player?.addEventListener(event, () =>
|
|
||||||
this.requestUpdate()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
handleMute(ev) {
|
|
||||||
window.browser_mod.player.muted = !window.browser_mod.player.muted;
|
|
||||||
}
|
|
||||||
handleVolumeChange(ev) {
|
|
||||||
const volume_level = parseFloat(ev.target.value);
|
|
||||||
window.browser_mod.player.volume = volume_level;
|
|
||||||
}
|
|
||||||
handleMoreInfo(ev) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("hass-more-info", {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
cancelable: false,
|
|
||||||
detail: {
|
|
||||||
entityId: window.browser_mod.browserEntities?.player,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
handlePlayPause(ev) {
|
|
||||||
if (
|
|
||||||
!window.browser_mod.player.src ||
|
|
||||||
window.browser_mod.player.paused ||
|
|
||||||
window.browser_mod.player.ended
|
|
||||||
) {
|
|
||||||
window.browser_mod.player.play();
|
|
||||||
window.browser_mod._show_video_player();
|
|
||||||
} else {
|
|
||||||
window.browser_mod.player.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!window.browser_mod) {
|
|
||||||
window.setTimeout(() => this.requestUpdate(), 100);
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
if (!window.browser_mod?.registered) {
|
|
||||||
return html`
|
|
||||||
<ha-card>
|
|
||||||
<ha-alert> This browser is not registered to Browser Mod. </ha-alert>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return html`
|
|
||||||
<ha-card>
|
|
||||||
<div class="card-content">
|
|
||||||
<ha-icon-button @click=${this.handleMute}>
|
|
||||||
<ha-icon
|
|
||||||
.icon=${window.browser_mod.player.muted
|
|
||||||
? "mdi:volume-off"
|
|
||||||
: "mdi:volume-high"}
|
|
||||||
></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
<ha-slider
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
step="0.01"
|
|
||||||
?disabled=${window.browser_mod.player.muted}
|
|
||||||
value=${window.browser_mod.player.volume}
|
|
||||||
@change=${this.handleVolumeChange}
|
|
||||||
></ha-slider>
|
|
||||||
|
|
||||||
${window.browser_mod.player_state === "stopped"
|
|
||||||
? html`<div class="placeholder"></div>`
|
|
||||||
: html`
|
|
||||||
<ha-icon-button @click=${this.handlePlayPause} highlight>
|
|
||||||
<ha-icon
|
|
||||||
.icon=${!window.browser_mod.player.src ||
|
|
||||||
window.browser_mod.player.ended ||
|
|
||||||
window.browser_mod.player.paused
|
|
||||||
? "mdi:play"
|
|
||||||
: "mdi:pause"}
|
|
||||||
></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
`}
|
|
||||||
<ha-icon-button @click=${this.handleMoreInfo}>
|
|
||||||
<ha-icon .icon=${"mdi:cog"}></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="browser-id">${window.browser_mod.browserID}</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
:host(["hidden"]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
:host([edit-mode="true"]) {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
paper-icon-button[highlight] {
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
width: 24px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
.browser-id {
|
|
||||||
opacity: 0.7;
|
|
||||||
font-size: xx-small;
|
|
||||||
margin-top: -10px;
|
|
||||||
user-select: all;
|
|
||||||
-webkit-user-select: all;
|
|
||||||
-moz-user-select: all;
|
|
||||||
-ms-user-select: all;
|
|
||||||
}
|
|
||||||
ha-icon-button ha-icon {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (async () => {
|
|
||||||
// while (!window.browser_mod) {
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
// }
|
|
||||||
// await window.browser_mod.connectionPromise;
|
|
||||||
|
|
||||||
if (!customElements.get("browser-player"))
|
|
||||||
customElements.define("browser-player", BrowserPlayer);
|
|
||||||
// })();
|
|
@ -1,49 +0,0 @@
|
|||||||
import { hass_base_el } from "../helpers";
|
|
||||||
|
|
||||||
export const BrowserStateMixin = (SuperClass) => {
|
|
||||||
return class BrowserStateMixinClass extends SuperClass {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
document.addEventListener("visibilitychange", () =>
|
|
||||||
this._browser_state_update()
|
|
||||||
);
|
|
||||||
window.addEventListener("location-changed", () =>
|
|
||||||
this._browser_state_update()
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addEventListener("fully-update", () => this._browser_state_update());
|
|
||||||
|
|
||||||
this.connectionPromise.then(() => this._browser_state_update());
|
|
||||||
}
|
|
||||||
|
|
||||||
_browser_state_update() {
|
|
||||||
const update = async () => {
|
|
||||||
const battery = (<any>navigator).getBattery?.();
|
|
||||||
this.sendUpdate({
|
|
||||||
browser: {
|
|
||||||
path: window.location.pathname,
|
|
||||||
visibility: document.visibilityState,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
currentUser: this.hass?.user?.name,
|
|
||||||
fullyKiosk: this.fully || false,
|
|
||||||
width: window.innerWidth,
|
|
||||||
height: window.innerHeight,
|
|
||||||
battery_level:
|
|
||||||
window.fully?.getBatteryLevel() ?? battery?.level * 100,
|
|
||||||
charging: window.fully?.isPlugged() ?? battery?.charging,
|
|
||||||
darkMode: this.hass?.themes?.darkMode,
|
|
||||||
userData: this.hass?.user,
|
|
||||||
ip_address: window.fully?.getIp4Address(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
async browser_navigate(path) {
|
|
||||||
if (!path) return;
|
|
||||||
history.pushState(null, "", path);
|
|
||||||
window.dispatchEvent(new CustomEvent("location-changed"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,61 +0,0 @@
|
|||||||
const ID_STORAGE_KEY = "browser_mod-browser-id";
|
|
||||||
|
|
||||||
export const BrowserIDMixin = (SuperClass) => {
|
|
||||||
return class BrowserIDMixinClass extends SuperClass {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
if (Storage) {
|
|
||||||
if (!Storage.prototype.browser_mod_patched) {
|
|
||||||
const _clear = Storage.prototype.clear;
|
|
||||||
Storage.prototype.clear = function () {
|
|
||||||
const browserId = this.getItem(ID_STORAGE_KEY);
|
|
||||||
const suspendWhenHidden = this.getItem("suspendWhenHidden");
|
|
||||||
_clear.apply(this);
|
|
||||||
this.setItem(ID_STORAGE_KEY, browserId);
|
|
||||||
this.setItem("suspendWhenHidden", suspendWhenHidden);
|
|
||||||
};
|
|
||||||
Storage.prototype.browser_mod_patched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async recall_id() {
|
|
||||||
// If the connection is still open, but the BrowserID has disappeared - recall it from the backend
|
|
||||||
// This happens e.g. when the frontend cache is reset in the Compainon app
|
|
||||||
if (!this.connection) return;
|
|
||||||
const recalledID = await this.connection.sendMessagePromise({
|
|
||||||
type: "browser_mod/recall_id",
|
|
||||||
});
|
|
||||||
if (recalledID) {
|
|
||||||
localStorage[ID_STORAGE_KEY] = recalledID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get browserID() {
|
|
||||||
if (document.querySelector("hc-main")) return "CAST";
|
|
||||||
if (localStorage[ID_STORAGE_KEY]) return localStorage[ID_STORAGE_KEY];
|
|
||||||
this.browserID = "";
|
|
||||||
this.recall_id();
|
|
||||||
return this.browserID;
|
|
||||||
}
|
|
||||||
set browserID(id) {
|
|
||||||
function _createBrowserID() {
|
|
||||||
const s4 = () => {
|
|
||||||
return Math.floor((1 + Math.random()) * 100000)
|
|
||||||
.toString(16)
|
|
||||||
.substring(1);
|
|
||||||
};
|
|
||||||
return window.fully?.getDeviceId() ?? `${s4()}${s4()}-${s4()}${s4()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id === "") id = _createBrowserID();
|
|
||||||
const oldID = localStorage[ID_STORAGE_KEY];
|
|
||||||
localStorage[ID_STORAGE_KEY] = id;
|
|
||||||
|
|
||||||
this.browserIDChanged(oldID, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected browserIDChanged(oldID, newID) {}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,99 +0,0 @@
|
|||||||
export const CameraMixin = (SuperClass) => {
|
|
||||||
return class CameraMixinClass extends SuperClass {
|
|
||||||
private _video;
|
|
||||||
private _canvas;
|
|
||||||
private _framerate;
|
|
||||||
public cameraError;
|
|
||||||
|
|
||||||
// TODO: Enable WebRTC?
|
|
||||||
// https://levelup.gitconnected.com/establishing-the-webrtc-connection-videochat-with-javascript-step-3-48d4ae0e9ea4
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this._framerate = 2;
|
|
||||||
this.cameraError = false;
|
|
||||||
|
|
||||||
this._setup_camera();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _setup_camera() {
|
|
||||||
if (this._video) return;
|
|
||||||
await this.connectionPromise;
|
|
||||||
await this.firstInteraction;
|
|
||||||
if (!this.cameraEnabled) return;
|
|
||||||
if (this.fully) return this.update_camera();
|
|
||||||
|
|
||||||
const div = document.createElement("div");
|
|
||||||
document.body.append(div);
|
|
||||||
div.classList.add("browser-mod-camera");
|
|
||||||
div.attachShadow({ mode: "open" });
|
|
||||||
|
|
||||||
const styleEl = document.createElement("style");
|
|
||||||
div.shadowRoot.append(styleEl);
|
|
||||||
styleEl.innerHTML = `
|
|
||||||
:host {
|
|
||||||
display: none;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const video = (this._video = document.createElement("video"));
|
|
||||||
div.shadowRoot.append(video);
|
|
||||||
video.autoplay = true;
|
|
||||||
video.playsInline = true;
|
|
||||||
video.style.display = "none";
|
|
||||||
|
|
||||||
const canvas = (this._canvas = document.createElement("canvas"));
|
|
||||||
div.shadowRoot.append(canvas);
|
|
||||||
canvas.style.display = "none";
|
|
||||||
|
|
||||||
if (!navigator.mediaDevices) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: true,
|
|
||||||
audio: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
video.srcObject = stream;
|
|
||||||
video.play();
|
|
||||||
this.update_camera();
|
|
||||||
} catch (e) {
|
|
||||||
if (e.name !== "NotAllowedError") throw e;
|
|
||||||
else {
|
|
||||||
this.cameraError = true;
|
|
||||||
this.fireEvent("browser-mod-config-update");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async update_camera() {
|
|
||||||
if (!this.cameraEnabled) {
|
|
||||||
const stream = this._video?.srcObject;
|
|
||||||
if (stream) {
|
|
||||||
stream.getTracks().forEach((t) => t.stop());
|
|
||||||
this._video.scrObject = undefined;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.fully) {
|
|
||||||
this.sendUpdate({
|
|
||||||
camera: this.fully_camera,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const video = this._video;
|
|
||||||
const width = video.videoWidth;
|
|
||||||
const height = video.videoHeight;
|
|
||||||
this._canvas.width = width;
|
|
||||||
this._canvas.height = height;
|
|
||||||
const context = this._canvas.getContext("2d");
|
|
||||||
context.drawImage(video, 0, 0, width, height);
|
|
||||||
|
|
||||||
this.sendUpdate({
|
|
||||||
camera: this._canvas.toDataURL("image/jpeg"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = Math.round(1000 / this._framerate);
|
|
||||||
setTimeout(() => this.update_camera(), interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,232 +0,0 @@
|
|||||||
import { hass, provideHass } from "../helpers";
|
|
||||||
|
|
||||||
export const ConnectionMixin = (SuperClass) => {
|
|
||||||
class BrowserModConnection extends SuperClass {
|
|
||||||
public hass;
|
|
||||||
public connection;
|
|
||||||
private _data;
|
|
||||||
public connected = false;
|
|
||||||
private _connectionResolve;
|
|
||||||
public connectionPromise = new Promise((resolve) => {
|
|
||||||
this._connectionResolve = resolve;
|
|
||||||
});
|
|
||||||
public browserEntities = {};
|
|
||||||
|
|
||||||
LOG(...args) {
|
|
||||||
if (window.browser_mod_log === undefined) return;
|
|
||||||
const dt = new Date();
|
|
||||||
console.log(`${dt.toLocaleTimeString()}`, ...args);
|
|
||||||
|
|
||||||
this.connection.sendMessage({
|
|
||||||
type: "browser_mod/log",
|
|
||||||
message: args[0],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private fireEvent(event, detail = undefined) {
|
|
||||||
this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private incoming_message(msg) {
|
|
||||||
if (msg.command) {
|
|
||||||
this.LOG("Command:", msg);
|
|
||||||
this.fireEvent(`command-${msg.command}`, msg);
|
|
||||||
} else if (msg.browserEntities) {
|
|
||||||
this.browserEntities = msg.browserEntities;
|
|
||||||
} else if (msg.result) {
|
|
||||||
this.update_config(msg.result);
|
|
||||||
}
|
|
||||||
this._connectionResolve?.();
|
|
||||||
this._connectionResolve = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private update_config(cfg) {
|
|
||||||
this.LOG("Receive:", cfg);
|
|
||||||
|
|
||||||
let update = false;
|
|
||||||
if (!this.registered && cfg.browsers?.[this.browserID]) {
|
|
||||||
update = true;
|
|
||||||
}
|
|
||||||
this._data = cfg;
|
|
||||||
|
|
||||||
if (!this.connected) {
|
|
||||||
this.connected = true;
|
|
||||||
this.fireEvent("browser-mod-connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fireEvent("browser-mod-config-update");
|
|
||||||
|
|
||||||
if (update) this.sendUpdate({});
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
const conn = (await hass()).connection;
|
|
||||||
this.connection = conn;
|
|
||||||
|
|
||||||
// Subscribe to configuration updates
|
|
||||||
conn.subscribeMessage((msg) => this.incoming_message(msg), {
|
|
||||||
type: "browser_mod/connect",
|
|
||||||
browserID: this.browserID,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep connection status up to date
|
|
||||||
conn.addEventListener("disconnected", () => {
|
|
||||||
this.connected = false;
|
|
||||||
this.fireEvent("browser-mod-disconnected");
|
|
||||||
});
|
|
||||||
conn.addEventListener("ready", () => {
|
|
||||||
this.connected = true;
|
|
||||||
this.fireEvent("browser-mod-connected");
|
|
||||||
this.sendUpdate({});
|
|
||||||
});
|
|
||||||
|
|
||||||
provideHass(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
get config() {
|
|
||||||
return this._data?.config ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
get browsers() {
|
|
||||||
return this._data?.browsers ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get registered() {
|
|
||||||
return this.browsers?.[this.browserID] !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
set registered(reg) {
|
|
||||||
(async () => {
|
|
||||||
if (reg) {
|
|
||||||
if (this.registered) return;
|
|
||||||
await this.connection.sendMessage({
|
|
||||||
type: "browser_mod/register",
|
|
||||||
browserID: this.browserID,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (!this.registered) return;
|
|
||||||
await this.connection.sendMessage({
|
|
||||||
type: "browser_mod/unregister",
|
|
||||||
browserID: this.browserID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _reregister(newData = {}) {
|
|
||||||
await this.connection.sendMessage({
|
|
||||||
type: "browser_mod/register",
|
|
||||||
browserID: this.browserID,
|
|
||||||
data: {
|
|
||||||
...this.browsers[this.browserID],
|
|
||||||
...newData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get global_settings() {
|
|
||||||
const settings = {};
|
|
||||||
const global = this._data?.settings ?? {};
|
|
||||||
for (const [k, v] of Object.entries(global)) {
|
|
||||||
if (v !== null) settings[k] = v;
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
get user_settings() {
|
|
||||||
const settings = {};
|
|
||||||
const user = this._data?.user_settings?.[this.hass?.user?.id] ?? {};
|
|
||||||
for (const [k, v] of Object.entries(user)) {
|
|
||||||
if (v !== null) settings[k] = v;
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
get browser_settings() {
|
|
||||||
const settings = {};
|
|
||||||
const browser = this.browsers?.[this.browserID]?.settings ?? {};
|
|
||||||
for (const [k, v] of Object.entries(browser)) {
|
|
||||||
if (v !== null) settings[k] = v;
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
get settings() {
|
|
||||||
return {
|
|
||||||
...this.global_settings,
|
|
||||||
...this.browser_settings,
|
|
||||||
...this.user_settings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
set_setting(key, value, level) {
|
|
||||||
switch (level) {
|
|
||||||
case "global": {
|
|
||||||
this.connection.sendMessage({
|
|
||||||
type: "browser_mod/settings",
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "user": {
|
|
||||||
const user = this.hass.user.id;
|
|
||||||
this.connection.sendMessage({
|
|
||||||
type: "browser_mod/settings",
|
|
||||||
user,
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "browser": {
|
|
||||||
const settings = this.browsers[this.browserID]?.settings;
|
|
||||||
settings[key] = value;
|
|
||||||
this._reregister({ settings });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get cameraEnabled() {
|
|
||||||
if (!this.registered) return null;
|
|
||||||
return this.browsers[this.browserID].camera;
|
|
||||||
}
|
|
||||||
set cameraEnabled(value) {
|
|
||||||
this._reregister({ camera: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
sendUpdate(data) {
|
|
||||||
if (!this.connected || !this.registered) return;
|
|
||||||
|
|
||||||
const dt = new Date();
|
|
||||||
this.LOG("Send:", data);
|
|
||||||
|
|
||||||
this.connection.sendMessage({
|
|
||||||
type: "browser_mod/update",
|
|
||||||
browserID: this.browserID,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
browserIDChanged(oldID, newID) {
|
|
||||||
this.fireEvent("browser-mod-config-update");
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.browsers?.[oldID] !== undefined &&
|
|
||||||
this.browsers?.[this.browserID] === undefined
|
|
||||||
) {
|
|
||||||
(async () => {
|
|
||||||
await this.connection.sendMessage({
|
|
||||||
type: "browser_mod/register",
|
|
||||||
browserID: oldID,
|
|
||||||
data: {
|
|
||||||
...this.browsers[oldID],
|
|
||||||
browserID: this.browserID,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return BrowserModConnection;
|
|
||||||
};
|
|
@ -1,224 +0,0 @@
|
|||||||
import { await_element, waitRepeat, runOnce, selectTree } from "../helpers";
|
|
||||||
|
|
||||||
export const AutoSettingsMixin = (SuperClass) => {
|
|
||||||
class AutoSettingsMixinClass extends SuperClass {
|
|
||||||
_faviconTemplateSubscription;
|
|
||||||
_titleTemplateSubscription;
|
|
||||||
_sidebarTitleSubscription;
|
|
||||||
__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() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
const runUpdates = async () => {
|
|
||||||
this.runUpdateTitle();
|
|
||||||
this.runHideHeader();
|
|
||||||
};
|
|
||||||
|
|
||||||
this._auto_settings_setup();
|
|
||||||
this.addEventListener("browser-mod-config-update", () => {
|
|
||||||
this._auto_settings_setup();
|
|
||||||
runUpdates();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("location-changed", runUpdates);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _auto_settings_setup() {
|
|
||||||
await this.connectionPromise;
|
|
||||||
|
|
||||||
const settings = this.settings;
|
|
||||||
|
|
||||||
// Sidebar panel order and hiding
|
|
||||||
if (settings.sidebarPanelOrder) {
|
|
||||||
localStorage.setItem("sidebarPanelOrder", settings.sidebarPanelOrder);
|
|
||||||
}
|
|
||||||
if (settings.sidebarHiddenPanels) {
|
|
||||||
localStorage.setItem(
|
|
||||||
"sidebarHiddenPanels",
|
|
||||||
settings.sidebarHiddenPanels
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default panel
|
|
||||||
if (settings.defaultPanel) {
|
|
||||||
localStorage.setItem("defaultPanel", `"${settings.defaultPanel}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide sidebar
|
|
||||||
if (settings.hideSidebar === true) {
|
|
||||||
selectTree(
|
|
||||||
document.body,
|
|
||||||
"home-assistant$home-assistant-main$app-drawer-layout"
|
|
||||||
).then((el) => el?.style?.setProperty("--app-drawer-width", "0px"));
|
|
||||||
selectTree(
|
|
||||||
document.body,
|
|
||||||
"home-assistant$home-assistant-main$app-drawer-layout app-drawer"
|
|
||||||
).then((el) => el?.remove?.());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sidebar title
|
|
||||||
if (settings.sidebarTitle) {
|
|
||||||
(async () => {
|
|
||||||
if (this._sidebarTitleSubscription) {
|
|
||||||
this._sidebarTitleSubscription();
|
|
||||||
}
|
|
||||||
this._sidebarTitleSubscription = undefined;
|
|
||||||
this._sidebarTitleSubscription =
|
|
||||||
await this.connection.subscribeMessage(this._updateSidebarTitle, {
|
|
||||||
type: "render_template",
|
|
||||||
template: settings.sidebarTitle,
|
|
||||||
variables: {},
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide header
|
|
||||||
|
|
||||||
// Favicon template
|
|
||||||
if (settings.faviconTemplate !== undefined) {
|
|
||||||
(async () => {
|
|
||||||
if (this._faviconTemplateSubscription) {
|
|
||||||
this._faviconTemplateSubscription();
|
|
||||||
}
|
|
||||||
this._faviconTemplateSubscription = undefined;
|
|
||||||
this._faviconTemplateSubscription =
|
|
||||||
await this.connection.subscribeMessage(this._updateFavicon, {
|
|
||||||
type: "render_template",
|
|
||||||
template: settings.faviconTemplate,
|
|
||||||
variables: {},
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title template
|
|
||||||
if (settings.titleTemplate !== undefined) {
|
|
||||||
(async () => {
|
|
||||||
if (this._titleTemplateSubscription) {
|
|
||||||
this._titleTemplateSubscription();
|
|
||||||
}
|
|
||||||
this._titleTemplateSubscription = undefined;
|
|
||||||
this._titleTemplateSubscription =
|
|
||||||
await this.connection.subscribeMessage(
|
|
||||||
this._updateTitle.bind(this),
|
|
||||||
{
|
|
||||||
type: "render_template",
|
|
||||||
template: settings.titleTemplate,
|
|
||||||
variables: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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() {
|
|
||||||
const link: any = document.head.querySelector("link[rel~='icon']");
|
|
||||||
return link?.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateFavicon({ result }) {
|
|
||||||
const link: any = document.head.querySelector("link[rel~='icon']");
|
|
||||||
link.href = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
get _currentTitle() {
|
|
||||||
return this.__currentTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateTitle(data = undefined) {
|
|
||||||
if (data) this.__currentTitle = data.result;
|
|
||||||
if (this.__currentTitle) document.title = this.__currentTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
@ -1,68 +0,0 @@
|
|||||||
export const FullyMixin = (C) => {
|
|
||||||
return class FullyMixinClass extends C {
|
|
||||||
private _fully_screensaver = false;
|
|
||||||
|
|
||||||
get fully() {
|
|
||||||
return window.fully !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
if (!this.fully) return;
|
|
||||||
|
|
||||||
for (const ev of [
|
|
||||||
"screenOn",
|
|
||||||
"screenOff",
|
|
||||||
"pluggedAC",
|
|
||||||
"pluggedUSB",
|
|
||||||
"onBatteryLevelChanged",
|
|
||||||
"unplugged",
|
|
||||||
"networkReconnect",
|
|
||||||
"onMotion",
|
|
||||||
"onDaydreamStart",
|
|
||||||
"onDaydreamStop",
|
|
||||||
]) {
|
|
||||||
window.fully.bind(ev, `window.browser_mod.fullyEvent("${ev}");`);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.fully.bind(
|
|
||||||
"onScreensaverStart",
|
|
||||||
`window.browser_mod._fully_screensaver = true; window.browser_mod.fullyEvent();`
|
|
||||||
);
|
|
||||||
window.fully.bind(
|
|
||||||
"onScreensaverStop",
|
|
||||||
`window.browser_mod._fully_screensaver = false; window.browser_mod.fullyEvent();`
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fully_screen() {
|
|
||||||
return this._fully_screensaver === false && window.fully?.getScreenOn();
|
|
||||||
}
|
|
||||||
set fully_screen(state) {
|
|
||||||
if (state) {
|
|
||||||
window.fully?.turnScreenOn();
|
|
||||||
window.fully?.stopScreensaver();
|
|
||||||
} else {
|
|
||||||
window.fully?.turnScreenOff();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get fully_brightness() {
|
|
||||||
return window.fully?.getScreenBrightness();
|
|
||||||
}
|
|
||||||
set fully_brightness(br) {
|
|
||||||
window.fully?.setScreenBrightness(br);
|
|
||||||
}
|
|
||||||
|
|
||||||
get fully_camera() {
|
|
||||||
return window.fully?.getCamshotJpgBase64();
|
|
||||||
}
|
|
||||||
|
|
||||||
fullyEvent(event = undefined) {
|
|
||||||
this.fireEvent("fully-update", { event });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,112 +0,0 @@
|
|||||||
import "./browser-player";
|
|
||||||
|
|
||||||
import { ConnectionMixin } from "./connection";
|
|
||||||
import { ScreenSaverMixin } from "./screensaver";
|
|
||||||
import { MediaPlayerMixin } from "./mediaPlayer";
|
|
||||||
import { CameraMixin } from "./camera";
|
|
||||||
import { RequireInteractMixin } from "./require-interact";
|
|
||||||
import { FullyMixin } from "./fullyKiosk";
|
|
||||||
import { BrowserStateMixin } from "./browser";
|
|
||||||
import { ServicesMixin } from "./services";
|
|
||||||
import { ActivityMixin } from "./activity";
|
|
||||||
import "./popups";
|
|
||||||
import { PopupMixin } from "./popups";
|
|
||||||
import pjson from "../../package.json";
|
|
||||||
import "./popup-card";
|
|
||||||
import { AutoSettingsMixin } from "./frontend-settings";
|
|
||||||
import { BrowserIDMixin } from "./browserID";
|
|
||||||
|
|
||||||
/*
|
|
||||||
TODO:
|
|
||||||
- More pictures for documentation
|
|
||||||
x Fix nomenclature
|
|
||||||
x Command -> Service
|
|
||||||
x Device -> Browser
|
|
||||||
- Popups
|
|
||||||
X Basic popups
|
|
||||||
x Styling
|
|
||||||
X Timeout
|
|
||||||
X Fullscreen
|
|
||||||
x Popup-card
|
|
||||||
x Auto-close
|
|
||||||
x Forms that are forwarded to service calls
|
|
||||||
x Motion/occupancy tracker
|
|
||||||
x Information about interaction requirement
|
|
||||||
x Information about fullykiosk
|
|
||||||
- Commands
|
|
||||||
x Change targets from the frontend
|
|
||||||
x Send browser ID to the backend in service calls?
|
|
||||||
x Rename browser_mod commands to browser_mod services
|
|
||||||
x Framework
|
|
||||||
x ll-custom handling
|
|
||||||
- Commands
|
|
||||||
x popup
|
|
||||||
x close_popup
|
|
||||||
x more-info
|
|
||||||
x navigate
|
|
||||||
o lovelace-reload?
|
|
||||||
o Not needed
|
|
||||||
x window-reload
|
|
||||||
o screensaver ?
|
|
||||||
o Refer to automations instead
|
|
||||||
x sequence
|
|
||||||
x delay
|
|
||||||
x javascript eval
|
|
||||||
o toast?
|
|
||||||
o Replaced with popups with timeout
|
|
||||||
x Redesign services to target devices
|
|
||||||
x frontend editor for popup cards
|
|
||||||
o also screensavers
|
|
||||||
x Saved frontend settings
|
|
||||||
X Framework
|
|
||||||
x Save sidebar
|
|
||||||
x Kiosk mode
|
|
||||||
x Default dashboard
|
|
||||||
o Screensaver?
|
|
||||||
x Favicon templates
|
|
||||||
x Title templates
|
|
||||||
- Tweaks
|
|
||||||
- Quickbar tweaks (ctrl+enter)?
|
|
||||||
x Card-mod preload
|
|
||||||
x Video player
|
|
||||||
x Media_seek
|
|
||||||
o Screensavers
|
|
||||||
x IMPORTANT: FIX DEFAULT HIDING OF ENTITIES
|
|
||||||
o NOFIX. Home Assistant bug
|
|
||||||
X Check functionality with CAST - may need to add frontend part as a lovelace resource
|
|
||||||
*/
|
|
||||||
export class BrowserMod extends ServicesMixin(
|
|
||||||
PopupMixin(
|
|
||||||
ActivityMixin(
|
|
||||||
BrowserStateMixin(
|
|
||||||
CameraMixin(
|
|
||||||
MediaPlayerMixin(
|
|
||||||
ScreenSaverMixin(
|
|
||||||
AutoSettingsMixin(
|
|
||||||
FullyMixin(
|
|
||||||
RequireInteractMixin(
|
|
||||||
ConnectionMixin(BrowserIDMixin(EventTarget))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.connect();
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
`%cBROWSER_MOD ${pjson.version} IS INSTALLED
|
|
||||||
%cBrowserID: ${this.browserID}`,
|
|
||||||
"color: green; font-weight: bold",
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!window.browser_mod) window.browser_mod = new BrowserMod();
|
|
@ -1,130 +0,0 @@
|
|||||||
import { selectTree, throttle } from "../helpers";
|
|
||||||
|
|
||||||
export const MediaPlayerMixin = (SuperClass) => {
|
|
||||||
class MediaPlayerMixinClass extends SuperClass {
|
|
||||||
public player;
|
|
||||||
private _audio_player;
|
|
||||||
private _video_player;
|
|
||||||
private _player_enabled;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this._audio_player = new Audio();
|
|
||||||
this._video_player = document.createElement("video");
|
|
||||||
this._video_player.controls = true;
|
|
||||||
this._video_player.style.setProperty("width", "100%");
|
|
||||||
this.player = this._audio_player;
|
|
||||||
this._player_enabled = false;
|
|
||||||
|
|
||||||
for (const ev of ["play", "pause", "ended", "volumechange"]) {
|
|
||||||
this._audio_player.addEventListener(ev, () => this._player_update());
|
|
||||||
this._video_player.addEventListener(ev, () => this._player_update());
|
|
||||||
}
|
|
||||||
for (const ev of ["timeupdate"]) {
|
|
||||||
this._audio_player.addEventListener(ev, () =>
|
|
||||||
this._player_update_throttled()
|
|
||||||
);
|
|
||||||
this._video_player.addEventListener(ev, () =>
|
|
||||||
this._player_update_throttled()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.firstInteraction.then(() => {
|
|
||||||
this._player_enabled = true;
|
|
||||||
if (!this.player.ended) this.player.play();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addEventListener("command-player-play", (ev) => {
|
|
||||||
if (this.player.src) this.player.pause();
|
|
||||||
if (ev.detail?.media_type)
|
|
||||||
if (ev.detail?.media_type.startsWith("video"))
|
|
||||||
this.player = this._video_player;
|
|
||||||
else this.player = this._audio_player;
|
|
||||||
if (ev.detail?.media_content_id)
|
|
||||||
this.player.src = ev.detail.media_content_id;
|
|
||||||
this.player.play();
|
|
||||||
this._show_video_player();
|
|
||||||
});
|
|
||||||
this.addEventListener("command-player-pause", (ev) =>
|
|
||||||
this.player.pause()
|
|
||||||
);
|
|
||||||
this.addEventListener("command-player-stop", (ev) => {
|
|
||||||
this.player.src = null;
|
|
||||||
this.player.pause();
|
|
||||||
});
|
|
||||||
this.addEventListener("command-player-set-volume", (ev) => {
|
|
||||||
if (ev.detail?.volume_level === undefined) return;
|
|
||||||
this.player.volume = ev.detail.volume_level;
|
|
||||||
});
|
|
||||||
this.addEventListener("command-player-mute", (ev) => {
|
|
||||||
if (ev.detail?.mute !== undefined)
|
|
||||||
this.player.muted = Boolean(ev.detail.mute);
|
|
||||||
else this.player.muted = !this.player.muted;
|
|
||||||
});
|
|
||||||
this.addEventListener("command-player-seek", (ev) => {
|
|
||||||
this.player.currentTime = ev.detail.position;
|
|
||||||
setTimeout(() => this._player_update(), 10);
|
|
||||||
});
|
|
||||||
this.addEventListener("command-player-turn-off", (ev) => {
|
|
||||||
if (
|
|
||||||
this.player === this._video_player &&
|
|
||||||
this._video_player.isConnected
|
|
||||||
)
|
|
||||||
this.closePopup();
|
|
||||||
else if (this.player.src) this.player.pause();
|
|
||||||
this.player.src = "";
|
|
||||||
this._player_update();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.connectionPromise.then(() => this._player_update());
|
|
||||||
}
|
|
||||||
|
|
||||||
private _show_video_player() {
|
|
||||||
if (this.player === this._video_player && this.player.src) {
|
|
||||||
selectTree(
|
|
||||||
document,
|
|
||||||
"home-assistant $ dialog-media-player-browse"
|
|
||||||
).then((el) => el?.closeDialog());
|
|
||||||
this.showPopup(undefined, this._video_player, {
|
|
||||||
dismiss_action: () => this._video_player.pause(),
|
|
||||||
size: "wide",
|
|
||||||
});
|
|
||||||
} else if (
|
|
||||||
this.player !== this._video_player &&
|
|
||||||
this._video_player.isConnected
|
|
||||||
) {
|
|
||||||
this.closePopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@throttle(3000)
|
|
||||||
_player_update_throttled() {
|
|
||||||
this._player_update();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _player_update() {
|
|
||||||
const state = this._player_enabled
|
|
||||||
? !this.player.src || this.player.src === window.location.href
|
|
||||||
? "off"
|
|
||||||
: this.player.ended
|
|
||||||
? "stopped"
|
|
||||||
: this.player.paused
|
|
||||||
? "paused"
|
|
||||||
: "playing"
|
|
||||||
: "unavailable";
|
|
||||||
this.sendUpdate({
|
|
||||||
player: {
|
|
||||||
volume: this.player.volume,
|
|
||||||
muted: this.player.muted,
|
|
||||||
src: this.player.src,
|
|
||||||
state,
|
|
||||||
media_duration: this.player.duration,
|
|
||||||
media_position: this.player.currentTime,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MediaPlayerMixinClass;
|
|
||||||
};
|
|
@ -1,270 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property, query, state } from "lit/decorators.js";
|
|
||||||
import { loadHaForm } from "../helpers";
|
|
||||||
|
|
||||||
const configSchema = [
|
|
||||||
{
|
|
||||||
name: "entity",
|
|
||||||
label: "Entity",
|
|
||||||
selector: { entity: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "title",
|
|
||||||
label: "Title",
|
|
||||||
selector: { text: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "size",
|
|
||||||
selector: {
|
|
||||||
select: { mode: "dropdown", options: ["normal", "wide", "fullscreen"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "grid",
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: "right_button",
|
|
||||||
label: "Right button",
|
|
||||||
selector: { text: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "left_button",
|
|
||||||
label: "Left button",
|
|
||||||
selector: { text: {} },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "grid",
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: "right_button_action",
|
|
||||||
label: "Right button action",
|
|
||||||
selector: { object: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "left_button_action",
|
|
||||||
label: "Left button action",
|
|
||||||
selector: { object: {} },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "grid",
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: "dismissable",
|
|
||||||
label: "User dismissable",
|
|
||||||
selector: { boolean: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "timeout",
|
|
||||||
label: "Auto close timeout (ms)",
|
|
||||||
selector: { number: { mode: "box" } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "grid",
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
name: "dismiss_action",
|
|
||||||
label: "Dismiss action",
|
|
||||||
selector: { object: {} },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "timeout_action",
|
|
||||||
label: "Timeout action",
|
|
||||||
selector: { object: {} },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "style",
|
|
||||||
label: "CSS style",
|
|
||||||
selector: { text: { multiline: true } },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
class PopupCardEditor extends LitElement {
|
|
||||||
@state() _config;
|
|
||||||
|
|
||||||
@property() lovelace;
|
|
||||||
@property() hass;
|
|
||||||
|
|
||||||
@state() _selectedTab = 0;
|
|
||||||
@state() _cardGUIMode = true;
|
|
||||||
@state() _cardGUIModeAvailable = true;
|
|
||||||
|
|
||||||
@query("hui-card-element-editor") private _cardEditorEl?;
|
|
||||||
|
|
||||||
setConfig(config) {
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
loadHaForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleSwitchTab(ev: CustomEvent) {
|
|
||||||
this._selectedTab = parseInt(ev.detail.index, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
_configChanged(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (!this._config) return;
|
|
||||||
this._config = { ...ev.detail.value };
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("config-changed", { detail: { config: this._config } })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_cardConfigChanged(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (!this._config) return;
|
|
||||||
const card = { ...ev.detail.config };
|
|
||||||
this._config = { ...this._config, card };
|
|
||||||
this._cardGUIModeAvailable = ev.detail.guiModeAvailable;
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("config-changed", { detail: { config: this._config } })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_toggleCardMode(ev) {
|
|
||||||
this._cardEditorEl?.toggleMode();
|
|
||||||
}
|
|
||||||
_deleteCard(ev) {
|
|
||||||
if (!this._config) return;
|
|
||||||
this._config = { ...this._config };
|
|
||||||
delete this._config.card;
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("config-changed", { detail: { config: this._config } })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_cardGUIModeChanged(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._cardGUIMode = ev.detail.guiMode;
|
|
||||||
this._cardGUIModeAvailable = ev.detail.guiModeAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.hass || !this._config) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="card-config">
|
|
||||||
<div class="toolbar">
|
|
||||||
<mwc-tab-bar
|
|
||||||
.activeIndex=${this._selectedTab}
|
|
||||||
@MDCTabBar:activated=${this._handleSwitchTab}
|
|
||||||
>
|
|
||||||
<mwc-tab .label=${"Settings"}></mwc-tab>
|
|
||||||
<mwc-tab .label=${"Card"}></mwc-tab>
|
|
||||||
</mwc-tab-bar>
|
|
||||||
</div>
|
|
||||||
<div id="editor">
|
|
||||||
${[this._renderSettingsEditor, this._renderCardEditor][
|
|
||||||
this._selectedTab
|
|
||||||
].bind(this)()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderSettingsEditor() {
|
|
||||||
return html`<div class="box">
|
|
||||||
<ha-form
|
|
||||||
.hass=${this.hass}
|
|
||||||
.data=${this._config}
|
|
||||||
.schema=${configSchema}
|
|
||||||
.computeLabel=${(s) => s.label ?? s.name}
|
|
||||||
@value-changed=${this._configChanged}
|
|
||||||
></ha-form>
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderCardEditor() {
|
|
||||||
return html`
|
|
||||||
<div class="box cards">
|
|
||||||
${this._config.card
|
|
||||||
? html`
|
|
||||||
<div class="toolbar">
|
|
||||||
<mwc-button
|
|
||||||
@click=${this._toggleCardMode}
|
|
||||||
.disabled=${!this._cardGUIModeAvailable}
|
|
||||||
class="gui-mode-button"
|
|
||||||
>
|
|
||||||
${!this._cardEditorEl || this._cardGUIMode
|
|
||||||
? "Show code editor"
|
|
||||||
: "Show visual editor"}
|
|
||||||
</mwc-button>
|
|
||||||
<mwc-button
|
|
||||||
.title=${"Change card type"}
|
|
||||||
@click=${this._deleteCard}
|
|
||||||
>
|
|
||||||
Change card type
|
|
||||||
</mwc-button>
|
|
||||||
</div>
|
|
||||||
<hui-card-element-editor
|
|
||||||
.hass=${this.hass}
|
|
||||||
.lovelace=${this.lovelace}
|
|
||||||
.value=${this._config.card}
|
|
||||||
@config-changed=${this._cardConfigChanged}
|
|
||||||
@GUImode-changed=${this._cardGUIModeChanged}
|
|
||||||
></hui-card-element-editor>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<hui-card-picker
|
|
||||||
.hass=${this.hass}
|
|
||||||
.lovelace=${this.lovelace}
|
|
||||||
@config-changed=${this._cardConfigChanged}
|
|
||||||
></hui-card-picker>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
mwc-tab-bar {
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
.box {
|
|
||||||
margin-top: 8px;
|
|
||||||
border: 1px solid var(--divider-color);
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
.box .toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
width: 100%;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.gui-mode-button {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
while (!window.browser_mod) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
await window.browser_mod.connectionPromise;
|
|
||||||
|
|
||||||
if (!customElements.get("popup-card-editor")) {
|
|
||||||
customElements.define("popup-card-editor", PopupCardEditor);
|
|
||||||
(window as any).customCards = (window as any).customCards || [];
|
|
||||||
(window as any).customCards.push({
|
|
||||||
type: "popup-card",
|
|
||||||
name: "Popup card",
|
|
||||||
preview: false,
|
|
||||||
description:
|
|
||||||
"Replace the more-info dialog for a given entity in the view that includes this card. (Browser Mod)",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
@ -1,200 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property, state } from "lit/decorators.js";
|
|
||||||
|
|
||||||
import "./popup-card-editor";
|
|
||||||
|
|
||||||
class PopupCard extends LitElement {
|
|
||||||
@property() hass;
|
|
||||||
@state() _config;
|
|
||||||
@property({ attribute: "edit-mode", reflect: true }) editMode;
|
|
||||||
@state() _element;
|
|
||||||
|
|
||||||
static getConfigElement() {
|
|
||||||
return document.createElement("popup-card-editor");
|
|
||||||
}
|
|
||||||
|
|
||||||
static getStubConfig(hass, entities) {
|
|
||||||
const entity = entities[0];
|
|
||||||
return {
|
|
||||||
entity,
|
|
||||||
title: "Custom popup",
|
|
||||||
dismissable: true,
|
|
||||||
card: { type: "markdown", content: "This replaces the more-info dialog" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.popup = this.popup.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig(config) {
|
|
||||||
this._config = config;
|
|
||||||
(async () => {
|
|
||||||
const ch = await window.loadCardHelpers();
|
|
||||||
this._element = await ch.createCardElement(config.card);
|
|
||||||
this._element.hass = this.hass;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
async connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
window.addEventListener("hass-more-info", this.popup);
|
|
||||||
|
|
||||||
if (this.parentElement.localName === "hui-card-preview") {
|
|
||||||
this.editMode = true;
|
|
||||||
this.removeAttribute("hidden");
|
|
||||||
} else {
|
|
||||||
this.setAttribute("hidden", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
window.removeEventListener("hass-more-info", this.popup);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup(ev: CustomEvent) {
|
|
||||||
if (
|
|
||||||
ev.detail?.entityId === this._config.entity &&
|
|
||||||
!ev.detail?.ignore_popup_card
|
|
||||||
) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
const config = { ...this._config };
|
|
||||||
delete config.card;
|
|
||||||
|
|
||||||
window.browser_mod?.service("popup", {
|
|
||||||
content: this._config.card,
|
|
||||||
...this._config,
|
|
||||||
});
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent("hass-more-info", {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
cancelable: false,
|
|
||||||
detail: { entityId: "." },
|
|
||||||
})
|
|
||||||
),
|
|
||||||
50
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updated(changedProperties) {
|
|
||||||
super.updated(changedProperties);
|
|
||||||
if (changedProperties.has("hass")) {
|
|
||||||
if (this._element) this._element.hass = this.hass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.editMode) return html``;
|
|
||||||
return html` <ha-card>
|
|
||||||
<div class="app-toolbar">
|
|
||||||
${this._config.dismissable
|
|
||||||
? html`
|
|
||||||
<ha-icon-button>
|
|
||||||
<ha-icon .icon=${"mdi:close"}></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div class="main-title">${this._config.title}</div>
|
|
||||||
</div>
|
|
||||||
${this._element}
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
${this._config.style}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
${this._config.right_button !== undefined ||
|
|
||||||
this._config.left_button !== undefined
|
|
||||||
? html`
|
|
||||||
<footer class="mdc-dialog__actions">
|
|
||||||
<span>
|
|
||||||
${this._config.left_button !== undefined
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
.label=${this._config.left_button}
|
|
||||||
></mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
${this._config.right_button !== undefined
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
.label=${this._config.right_button}
|
|
||||||
></mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</ha-card>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
:host([edit-mode="true"]) {
|
|
||||||
display: block !important;
|
|
||||||
border: 1px solid var(--primary-color);
|
|
||||||
}
|
|
||||||
ha-card {
|
|
||||||
background-color: var(
|
|
||||||
--popup-background-color,
|
|
||||||
var(--ha-card-background, var(--card-background-color, white))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.app-toolbar {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
background-color: var(
|
|
||||||
--popup-header-background-color,
|
|
||||||
var(--popup-background-color, --sidebar-background-color)
|
|
||||||
);
|
|
||||||
display: var(--layout-horizontal_-_display);
|
|
||||||
flex-direction: var(--layout-horizontal_-_flex-direction);
|
|
||||||
align-items: var(--layout-center_-_align-items);
|
|
||||||
height: 64px;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: var(--app-toolbar-font-size, 20px);
|
|
||||||
}
|
|
||||||
ha-icon-button > * {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.main-title {
|
|
||||||
margin-left: 16px;
|
|
||||||
line-height: 1.3em;
|
|
||||||
max-height: 2.6em;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mdc-dialog__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
min-height: 52px;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 8px;
|
|
||||||
border-top: 1px solid transparent;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
while (!window.browser_mod) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
await window.browser_mod.connectionPromise;
|
|
||||||
|
|
||||||
if (!customElements.get("popup-card"))
|
|
||||||
customElements.define("popup-card", PopupCard);
|
|
||||||
})();
|
|
@ -1,413 +0,0 @@
|
|||||||
import { LitElement, html, css } from "lit";
|
|
||||||
import { property, query } from "lit/decorators.js";
|
|
||||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
|
||||||
import { provideHass, loadLoadCardHelpers, hass_base_el } from "../helpers";
|
|
||||||
import { loadHaForm } from "../helpers";
|
|
||||||
|
|
||||||
class BrowserModPopup extends LitElement {
|
|
||||||
@property() open;
|
|
||||||
@property() content;
|
|
||||||
@property() title;
|
|
||||||
@property({ reflect: true }) actions;
|
|
||||||
@property({ reflect: true }) card;
|
|
||||||
@property() right_button;
|
|
||||||
@property() left_button;
|
|
||||||
@property() dismissable;
|
|
||||||
@property({ reflect: true }) wide;
|
|
||||||
@property({ reflect: true }) fullscreen;
|
|
||||||
@property() _style;
|
|
||||||
@query("ha-dialog") dialog: any;
|
|
||||||
_autoclose;
|
|
||||||
_autocloseListener;
|
|
||||||
_actions;
|
|
||||||
timeout;
|
|
||||||
_timeoutStart;
|
|
||||||
_timeoutTimer;
|
|
||||||
_resolveClosed;
|
|
||||||
_formdata;
|
|
||||||
|
|
||||||
async closeDialog() {
|
|
||||||
this.open = false;
|
|
||||||
this.card = undefined;
|
|
||||||
clearInterval(this._timeoutTimer);
|
|
||||||
if (this._autocloseListener) {
|
|
||||||
window.browser_mod.removeEventListener(
|
|
||||||
"browser-mod-activity",
|
|
||||||
this._autocloseListener
|
|
||||||
);
|
|
||||||
this._autocloseListener = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
openDialog() {
|
|
||||||
this.open = true;
|
|
||||||
this.dialog?.show();
|
|
||||||
if (this.timeout) {
|
|
||||||
this._timeoutStart = new Date().getTime();
|
|
||||||
this._timeoutTimer = setInterval(() => {
|
|
||||||
const ellapsed = new Date().getTime() - this._timeoutStart;
|
|
||||||
const progress = (ellapsed / this.timeout) * 100;
|
|
||||||
this.style.setProperty("--progress", `${progress}%`);
|
|
||||||
if (ellapsed >= this.timeout) this._timeout();
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
this._autocloseListener = undefined;
|
|
||||||
if (this._autoclose) {
|
|
||||||
this._autocloseListener = () => this.dialog.close();
|
|
||||||
window.browser_mod.addEventListener(
|
|
||||||
"browser-mod-activity",
|
|
||||||
this._autocloseListener,
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
size = undefined,
|
|
||||||
style = undefined,
|
|
||||||
autoclose = false,
|
|
||||||
} = {}
|
|
||||||
) {
|
|
||||||
this._formdata = undefined;
|
|
||||||
this.title = title;
|
|
||||||
this.card = undefined;
|
|
||||||
if (content && content instanceof HTMLElement) {
|
|
||||||
this.content = content;
|
|
||||||
} else if (content && Array.isArray(content)) {
|
|
||||||
loadHaForm();
|
|
||||||
const form: any = document.createElement("ha-form");
|
|
||||||
form.schema = content;
|
|
||||||
form.computeLabel = (s) => s.label ?? s.name;
|
|
||||||
form.hass = window.browser_mod.hass;
|
|
||||||
this._formdata = {};
|
|
||||||
for (const i of content) {
|
|
||||||
if (i.name && i.default !== undefined) {
|
|
||||||
this._formdata[i.name] = i.default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
form.data = this._formdata;
|
|
||||||
provideHass(form);
|
|
||||||
form.addEventListener("value-changed", (ev) => {
|
|
||||||
this._formdata = { ...ev.detail.value };
|
|
||||||
form.data = this._formdata;
|
|
||||||
});
|
|
||||||
this.content = form;
|
|
||||||
} else if (content && typeof content === "object") {
|
|
||||||
// Create a card from config in content
|
|
||||||
this.card = true;
|
|
||||||
const helpers = await window.loadCardHelpers();
|
|
||||||
const card = await helpers.createCardElement(content);
|
|
||||||
card.hass = window.browser_mod.hass;
|
|
||||||
provideHass(card);
|
|
||||||
this.content = card;
|
|
||||||
} else {
|
|
||||||
// Basic HTML content
|
|
||||||
this.content = unsafeHTML(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.right_button = right_button;
|
|
||||||
this.left_button = left_button;
|
|
||||||
this.actions = right_button === undefined ? undefined : "";
|
|
||||||
|
|
||||||
this.dismissable = dismissable;
|
|
||||||
this.timeout = timeout;
|
|
||||||
this._actions = {
|
|
||||||
right_button_action,
|
|
||||||
left_button_action,
|
|
||||||
dismiss_action,
|
|
||||||
timeout_action,
|
|
||||||
};
|
|
||||||
this.wide = size === "wide" ? "" : undefined;
|
|
||||||
this.fullscreen = size === "fullscreen" ? "" : undefined;
|
|
||||||
this._style = style;
|
|
||||||
this._autoclose = autoclose;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _primary() {
|
|
||||||
if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined;
|
|
||||||
this.dialog?.close();
|
|
||||||
this._actions?.right_button_action?.(this._formdata);
|
|
||||||
}
|
|
||||||
async _secondary() {
|
|
||||||
if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined;
|
|
||||||
this.dialog?.close();
|
|
||||||
this._actions?.left_button_action?.(this._formdata);
|
|
||||||
}
|
|
||||||
async _dismiss(ev?) {
|
|
||||||
this.dialog?.close();
|
|
||||||
this._actions?.dismiss_action?.();
|
|
||||||
}
|
|
||||||
async _timeout() {
|
|
||||||
if (this._actions?.dismiss_action) this._actions.dismiss_action = undefined;
|
|
||||||
this.dialog?.close();
|
|
||||||
this._actions?.timeout_action?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.open) return html``;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-dialog
|
|
||||||
open
|
|
||||||
@closed=${this.closeDialog}
|
|
||||||
@closing=${this._dismiss}
|
|
||||||
.heading=${this.title !== undefined}
|
|
||||||
?hideActions=${this.actions === undefined}
|
|
||||||
.scrimClickAction=${this.dismissable ? "close" : ""}
|
|
||||||
.escapeKeyAction=${this.dismissable ? "close" : ""}
|
|
||||||
>
|
|
||||||
${this.timeout
|
|
||||||
? html` <div slot="heading" class="progress"></div> `
|
|
||||||
: ""}
|
|
||||||
${this.title
|
|
||||||
? html`
|
|
||||||
<div slot="heading">
|
|
||||||
<ha-header-bar>
|
|
||||||
${this.dismissable
|
|
||||||
? html`
|
|
||||||
<ha-icon-button
|
|
||||||
dialogAction="cancel"
|
|
||||||
slot="navigationIcon"
|
|
||||||
>
|
|
||||||
<ha-icon .icon=${"mdi:close"}></ha-icon>
|
|
||||||
</ha-icon-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div slot="title" class="main-title">${this.title}</div>
|
|
||||||
</ha-header-bar>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html``}
|
|
||||||
|
|
||||||
<div class="content">${this.content}</div>
|
|
||||||
|
|
||||||
${this.right_button !== undefined
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
slot="primaryAction"
|
|
||||||
.label=${this.right_button}
|
|
||||||
@click=${this._primary}
|
|
||||||
class="action-button"
|
|
||||||
></mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this.left_button !== undefined
|
|
||||||
? html`
|
|
||||||
<mwc-button
|
|
||||||
slot="secondaryAction"
|
|
||||||
.label=${this.left_button}
|
|
||||||
@click=${this._secondary}
|
|
||||||
class="action-button"
|
|
||||||
></mwc-button>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<style>
|
|
||||||
:host {
|
|
||||||
${this._style}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles() {
|
|
||||||
return css`
|
|
||||||
ha-dialog {
|
|
||||||
z-index: 10;
|
|
||||||
--mdc-dialog-min-width: var(--popup-min-width, 400px);
|
|
||||||
--mdc-dialog-max-width: var(--popup-max-width, 600px);
|
|
||||||
--mdc-dialog-heading-ink-color: var(--primary-text-color);
|
|
||||||
--mdc-dialog-content-ink-color: var(--primary-text-color);
|
|
||||||
--justify-action-buttons: space-between;
|
|
||||||
|
|
||||||
--dialog-box-shadow: 0px 0px 0px
|
|
||||||
var(--popup-border-width, var(--ha-card-border-width, 2px))
|
|
||||||
var(
|
|
||||||
--popup-border-color,
|
|
||||||
var(--ha-card-border-color, var(--divider-color, #e0e0e0))
|
|
||||||
);
|
|
||||||
--mdc-theme-surface: var(
|
|
||||||
--popup-background-color,
|
|
||||||
var(--ha-card-background, var(--card-background-color, white))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
:host([wide]) ha-dialog {
|
|
||||||
--mdc-dialog-max-width: 90vw;
|
|
||||||
}
|
|
||||||
:host([fullscreen]) ha-dialog {
|
|
||||||
--mdc-dialog-min-width: 100vw;
|
|
||||||
--mdc-dialog-max-width: 100vw;
|
|
||||||
--mdc-dialog-min-height: 100%;
|
|
||||||
--mdc-dialog-max-height: 100%;
|
|
||||||
--mdc-shape-medium: 0px;
|
|
||||||
--vertial-align-dialog: flex-end;
|
|
||||||
--ha-dialog-border-radius: 0px;
|
|
||||||
}
|
|
||||||
.progress::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
width: calc(100% - var(--progress, 60%));
|
|
||||||
top: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--primary-color);
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-header-bar {
|
|
||||||
--mdc-theme-on-primary: var(--primary-text-color);
|
|
||||||
--mdc-theme-primary: var(--mdc-theme-surface);
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-icon-button > * {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.main-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
--padding-x: 24px;
|
|
||||||
--padding-y: 20px;
|
|
||||||
margin: -20px -24px;
|
|
||||||
padding: var(--padding-y) var(--padding-y);
|
|
||||||
--header-height: 64px;
|
|
||||||
--footer-height: 0px;
|
|
||||||
}
|
|
||||||
.content:first-child {
|
|
||||||
--header-height: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:host([card]) .content {
|
|
||||||
--padding-x: 0px;
|
|
||||||
--padding-y: 0px;
|
|
||||||
--ha-card-box-shadow: none;
|
|
||||||
}
|
|
||||||
:host([actions]) .content {
|
|
||||||
xborder-bottom: 2px solid
|
|
||||||
var(--popup-border-color, var(--divider-color));
|
|
||||||
--footer-height: 54px;
|
|
||||||
}
|
|
||||||
:host([wide]) .content {
|
|
||||||
width: calc(90vw - 2 * var(--padding-x));
|
|
||||||
}
|
|
||||||
:host([fullscreen]) .content {
|
|
||||||
height: calc(
|
|
||||||
100vh - var(--header-height) - var(--footer-height) - 2 *
|
|
||||||
var(--padding-y) - 16px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
margin-bottom: -24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
|
||||||
ha-dialog {
|
|
||||||
--mdc-dialog-min-width: 100vw;
|
|
||||||
--mdc-dialog-max-width: 100vw;
|
|
||||||
--mdc-shape-medium: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!customElements.get("browser-mod-popup"))
|
|
||||||
customElements.define("browser-mod-popup", BrowserModPopup);
|
|
||||||
|
|
||||||
export const PopupMixin = (SuperClass) => {
|
|
||||||
return class PopupMixinClass extends SuperClass {
|
|
||||||
private _popupEl: any;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
loadLoadCardHelpers();
|
|
||||||
|
|
||||||
this._popupEl = document.createElement("browser-mod-popup");
|
|
||||||
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 popupState = ev.state?.browserModPopup;
|
|
||||||
// if (popupState) {
|
|
||||||
// if (popupState.open) {
|
|
||||||
// this._popupEl.setupDialog(...popupState.args);
|
|
||||||
// this._popupEl.openDialog();
|
|
||||||
// } else {
|
|
||||||
// this._popupEl.closeDialog();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// window.addEventListener("popstate", historyListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
showPopup(...args) {
|
|
||||||
// if (history.state?.browserModPopup === undefined) {
|
|
||||||
// history.replaceState(
|
|
||||||
// {
|
|
||||||
// browserModPopup: {
|
|
||||||
// open: false,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ""
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// history.pushState(
|
|
||||||
// {
|
|
||||||
// browserModPopup: {
|
|
||||||
// open: true,
|
|
||||||
// args,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ""
|
|
||||||
// );
|
|
||||||
this._popupEl.setupDialog(...args).then(() => this._popupEl.openDialog());
|
|
||||||
}
|
|
||||||
|
|
||||||
closePopup(...args) {
|
|
||||||
this._popupEl.closeDialog();
|
|
||||||
this.showMoreInfo("");
|
|
||||||
}
|
|
||||||
|
|
||||||
async showMoreInfo(entityId, large = false, ignore_popup_card = undefined) {
|
|
||||||
const base = await hass_base_el();
|
|
||||||
base.dispatchEvent(
|
|
||||||
new CustomEvent("hass-more-info", {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
cancelable: false,
|
|
||||||
detail: { entityId, ignore_popup_card },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
if (large) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
const dialog: any = base.shadowRoot.querySelector(
|
|
||||||
"ha-more-info-dialog"
|
|
||||||
);
|
|
||||||
if (dialog) dialog.large = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,84 +0,0 @@
|
|||||||
export const RequireInteractMixin = (SuperClass) => {
|
|
||||||
return class RequireInteractMixinClass extends SuperClass {
|
|
||||||
private _interactionResolve;
|
|
||||||
public firstInteraction = new Promise((resolve) => {
|
|
||||||
this._interactionResolve = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.show_indicator();
|
|
||||||
}
|
|
||||||
|
|
||||||
async show_indicator() {
|
|
||||||
await this.connectionPromise;
|
|
||||||
|
|
||||||
if (!this.registered) return;
|
|
||||||
|
|
||||||
const interactSymbol = document.createElement("div");
|
|
||||||
document.body.append(interactSymbol);
|
|
||||||
|
|
||||||
interactSymbol.classList.add("browser-mod-require-interaction");
|
|
||||||
interactSymbol.attachShadow({ mode: "open" });
|
|
||||||
|
|
||||||
const styleEl = document.createElement("style");
|
|
||||||
interactSymbol.shadowRoot.append(styleEl);
|
|
||||||
styleEl.innerHTML = `
|
|
||||||
:host {
|
|
||||||
position: fixed;
|
|
||||||
right: 8px;
|
|
||||||
bottom: 8px;
|
|
||||||
color: var(--warning-color, red);
|
|
||||||
opacity: 0.5;
|
|
||||||
--mdc-icon-size: 48px;
|
|
||||||
}
|
|
||||||
ha-icon::before {
|
|
||||||
content: "Browser\\00a0Mod";
|
|
||||||
font-size: 0.75rem;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 90%;
|
|
||||||
}
|
|
||||||
video {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const icon = document.createElement("ha-icon");
|
|
||||||
interactSymbol.shadowRoot.append(icon);
|
|
||||||
(icon as any).icon = "mdi:gesture-tap";
|
|
||||||
|
|
||||||
// If we are allowed to play a video, we can assume no interaction is needed
|
|
||||||
const video = (this._video = document.createElement("video"));
|
|
||||||
interactSymbol.shadowRoot.append(video);
|
|
||||||
const vPlay = video.play();
|
|
||||||
if (vPlay) {
|
|
||||||
vPlay
|
|
||||||
.then(() => {
|
|
||||||
this._interactionResolve();
|
|
||||||
video.pause();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
if (e.name === "AbortError") {
|
|
||||||
this._interactionResolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
video.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener(
|
|
||||||
"pointerdown",
|
|
||||||
() => {
|
|
||||||
this._interactionResolve();
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// if (this.fully) this._interactionResolve();
|
|
||||||
|
|
||||||
await this.firstInteraction;
|
|
||||||
interactSymbol.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,97 +0,0 @@
|
|||||||
export const ScreenSaverMixin = (SuperClass) => {
|
|
||||||
class ScreenSaverMixinClass extends SuperClass {
|
|
||||||
private _panel;
|
|
||||||
private _listeners = {};
|
|
||||||
private _brightness = 255;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
const panel = (this._panel = document.createElement("div"));
|
|
||||||
document.body.append(panel);
|
|
||||||
panel.classList.add("browser-mod-blackout");
|
|
||||||
panel.attachShadow({ mode: "open" });
|
|
||||||
|
|
||||||
const styleEl = document.createElement("style");
|
|
||||||
panel.shadowRoot.append(styleEl);
|
|
||||||
styleEl.innerHTML = `
|
|
||||||
:host {
|
|
||||||
background: rgba(0,0,0, var(--darkness));
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 10000;
|
|
||||||
display: block;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
:host([dark]) {
|
|
||||||
background: rgba(0,0,0,1);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
this.addEventListener("command-screen_off", () => this._screen_off());
|
|
||||||
this.addEventListener("command-screen_on", (ev) => this._screen_on(ev));
|
|
||||||
|
|
||||||
this.addEventListener("fully-update", () => this.send_screen_status());
|
|
||||||
|
|
||||||
this.connectionPromise.then(() => this._screen_on());
|
|
||||||
}
|
|
||||||
|
|
||||||
send_screen_status() {
|
|
||||||
let screen_on = !this._panel.hasAttribute("dark");
|
|
||||||
let screen_brightness = this._brightness;
|
|
||||||
if (this.fully) {
|
|
||||||
screen_on = this.fully_screen;
|
|
||||||
screen_brightness = this.fully_brightness;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendUpdate({ screen_on, screen_brightness });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _screen_off() {
|
|
||||||
if (this.fully) {
|
|
||||||
this.fully_screen = false;
|
|
||||||
} else {
|
|
||||||
this._panel.setAttribute("dark", "");
|
|
||||||
}
|
|
||||||
this.send_screen_status();
|
|
||||||
|
|
||||||
const l = () => this._screen_on();
|
|
||||||
for (const ev of ["pointerdown", "pointermove", "keydown"]) {
|
|
||||||
this._listeners[ev] = l;
|
|
||||||
window.addEventListener(ev, l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _screen_on(ev = undefined) {
|
|
||||||
if (this.fully) {
|
|
||||||
this.fully_screen = true;
|
|
||||||
if (ev?.detail?.brightness) {
|
|
||||||
this.fully_brightness = ev.detail.brightness;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (ev?.detail?.brightness) {
|
|
||||||
this._brightness = ev.detail.brightness;
|
|
||||||
this._panel.style.setProperty(
|
|
||||||
"--darkness",
|
|
||||||
1 - ev.detail.brightness / 255
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this._panel.removeAttribute("dark");
|
|
||||||
}
|
|
||||||
this.send_screen_status();
|
|
||||||
|
|
||||||
for (const ev of ["pointerdown", "pointermove", "keydown"]) {
|
|
||||||
if (this._listeners[ev]) {
|
|
||||||
window.removeEventListener(ev, this._listeners[ev]);
|
|
||||||
this._listeners[ev] = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ScreenSaverMixinClass;
|
|
||||||
};
|
|
@ -1,175 +0,0 @@
|
|||||||
export const ServicesMixin = (SuperClass) => {
|
|
||||||
return class ServicesMixinClass extends SuperClass {
|
|
||||||
/*
|
|
||||||
Structure of service call:
|
|
||||||
service: <service>
|
|
||||||
[data: <data>]
|
|
||||||
|
|
||||||
Sequence:
|
|
||||||
service: browser_mod.sequence
|
|
||||||
data:
|
|
||||||
sequence:
|
|
||||||
- <service call>
|
|
||||||
- <service call>
|
|
||||||
- ...
|
|
||||||
|
|
||||||
Delay
|
|
||||||
service: browser_mod.delay
|
|
||||||
data:
|
|
||||||
time: <number>
|
|
||||||
|
|
||||||
Popup:
|
|
||||||
service: browser_mod.popup
|
|
||||||
data:
|
|
||||||
[title: <string>]
|
|
||||||
[content: <string | Lovelace Card Configuration>]
|
|
||||||
[size: <NORMAL/wide/fullscreen>]
|
|
||||||
[right_button: <string>]
|
|
||||||
[right_button_action: <service call>]
|
|
||||||
[left_button: <string>]
|
|
||||||
[left_button_action: <service call>]
|
|
||||||
[dismissable: <TRUE/false>]
|
|
||||||
[dismiss_action: <service call>]
|
|
||||||
[autoclose: <true/FALSE>]
|
|
||||||
[timeout: <number>]
|
|
||||||
[timeout_action: <service call>]
|
|
||||||
[style: <string>]
|
|
||||||
|
|
||||||
More-info:
|
|
||||||
service: browser_mod.more_info
|
|
||||||
data:
|
|
||||||
entity: <string>
|
|
||||||
[large: <true/FALSE>]
|
|
||||||
[ignore_popup_card: <true/FALSE>]
|
|
||||||
|
|
||||||
Close popup:
|
|
||||||
service: browser_mod.close_popup
|
|
||||||
|
|
||||||
Navigate to path:
|
|
||||||
service: browser_mod.navigate
|
|
||||||
data:
|
|
||||||
path: <string>
|
|
||||||
|
|
||||||
Refresh browser:
|
|
||||||
service: browser_mod.refresh
|
|
||||||
|
|
||||||
Browser console print:
|
|
||||||
service: browser_mod.console
|
|
||||||
data:
|
|
||||||
message: <string>
|
|
||||||
|
|
||||||
Run javascript:
|
|
||||||
service: browser_mod.javascript
|
|
||||||
data:
|
|
||||||
code: <string>
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
const cmds = [
|
|
||||||
"sequence",
|
|
||||||
"delay",
|
|
||||||
"popup",
|
|
||||||
"more_info",
|
|
||||||
"close_popup",
|
|
||||||
"navigate",
|
|
||||||
"refresh",
|
|
||||||
"console",
|
|
||||||
"javascript",
|
|
||||||
];
|
|
||||||
for (const service of cmds) {
|
|
||||||
this.addEventListener(`command-${service}`, (ev) => {
|
|
||||||
this.service(service, ev.detail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.addEventListener("ll-custom", (ev: CustomEvent) => {
|
|
||||||
if (ev.detail.browser_mod) {
|
|
||||||
this._service_action(ev.detail.browser_mod);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async service(service, data) {
|
|
||||||
this._service_action({ service, data });
|
|
||||||
}
|
|
||||||
|
|
||||||
async _service_action({ service, data }) {
|
|
||||||
let _service: String = service;
|
|
||||||
if (
|
|
||||||
(!_service.startsWith("browser_mod.") && _service.includes(".")) ||
|
|
||||||
data.browser_id !== undefined
|
|
||||||
) {
|
|
||||||
const d = { ...data };
|
|
||||||
if (d.browser_id === "THIS") d.browser_id = this.browserID;
|
|
||||||
// CALL HOME ASSISTANT SERVICE
|
|
||||||
const [domain, srv] = _service.split(".");
|
|
||||||
return this.hass.callService(domain, srv, d);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 "more_info":
|
|
||||||
const { entity, large, ignore_popup_card } = data;
|
|
||||||
this.showMoreInfo(entity, large, ignore_popup_card);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "popup":
|
|
||||||
const { title, content, ...d } = data;
|
|
||||||
for (const [k, v] of Object.entries(d)) {
|
|
||||||
if (k.endsWith("_action")) {
|
|
||||||
d[k] = (ext_data?) => {
|
|
||||||
const { service, data } = v as any;
|
|
||||||
this._service_action({
|
|
||||||
service,
|
|
||||||
data: { ...data, ...ext_data },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.showPopup(title, content, d);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "close_popup":
|
|
||||||
this.closePopup();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "navigate":
|
|
||||||
this.browser_navigate(data.path);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "refresh":
|
|
||||||
window.location.href = window.location.href;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "console":
|
|
||||||
if (
|
|
||||||
Object.keys(data).length > 1 ||
|
|
||||||
(data && data.message === undefined)
|
|
||||||
)
|
|
||||||
console.dir(data);
|
|
||||||
else console.log(data.message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "javascript":
|
|
||||||
const code = `
|
|
||||||
"use strict";
|
|
||||||
${data.code}
|
|
||||||
`;
|
|
||||||
const fn = new Function("hass", "data", code);
|
|
||||||
fn(this.hass, data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,41 +0,0 @@
|
|||||||
const a = {};
|
|
||||||
|
|
||||||
import { BrowserMod } from "./main";
|
|
||||||
|
|
||||||
interface FullyKiosk {
|
|
||||||
// Get device info
|
|
||||||
getIp4Address: { (): String };
|
|
||||||
getDeviceId: { (): String };
|
|
||||||
getBatteryLevel: { (): Number };
|
|
||||||
getScreenBrightness: { (): Number };
|
|
||||||
getScreenOn: { (): Boolean };
|
|
||||||
isPlugged: { (): Boolean };
|
|
||||||
|
|
||||||
// Controll device, show notifications, send network data etc.
|
|
||||||
turnScreenOn: { () };
|
|
||||||
turnScreenOff: { (keepAlive?: Boolean) };
|
|
||||||
setScreenBrightness: { (lewvel: Number) };
|
|
||||||
|
|
||||||
// Control fully and browsing
|
|
||||||
startScreensaver: { () };
|
|
||||||
stopScreensaver: { () };
|
|
||||||
|
|
||||||
// Respond to events
|
|
||||||
bind: { (event: String, action: String) };
|
|
||||||
|
|
||||||
// Motion detection
|
|
||||||
getCamshotJpgBase64: { (): String };
|
|
||||||
|
|
||||||
getBooleanSetting: { (key: String): String };
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
browser_mod?: BrowserMod;
|
|
||||||
browser_mod_log?: any;
|
|
||||||
fully?: FullyKiosk;
|
|
||||||
hassConnection?: Promise<any>;
|
|
||||||
customCards?: [{}?];
|
|
||||||
loadCardHelpers?: { () };
|
|
||||||
}
|
|
||||||
}
|
|
4632
package-lock.json
generated
4632
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -1,25 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "browser_mod",
|
"name": "browser_mod",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "webpack",
|
||||||
"watch": "rollup -c --watch"
|
"watch": "webpack --watch --mode=development",
|
||||||
|
"update-card-tools": "npm uninstall card-tools && npm install thomasloven/lovelace-card-tools"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Thomas Lovén",
|
"author": "Thomas Lovén",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.9",
|
"webpack": "^4.41.2",
|
||||||
"@rollup/plugin-babel": "^5.3.1",
|
"webpack-cli": "^3.3.10"
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
|
||||||
"lit": "^2.2.8",
|
|
||||||
"rollup": "^2.77.2",
|
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
|
||||||
"rollup-plugin-typescript2": "^0.32.1",
|
|
||||||
"typescript": "^4.7.4"
|
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {
|
||||||
|
"card-tools": "github:thomasloven/lovelace-card-tools"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import nodeResolve from "@rollup/plugin-node-resolve";
|
|
||||||
import json from "@rollup/plugin-json";
|
|
||||||
import typescript from "rollup-plugin-typescript2";
|
|
||||||
import { terser } from "rollup-plugin-terser";
|
|
||||||
import babel from "@rollup/plugin-babel";
|
|
||||||
|
|
||||||
const dev = process.env.ROLLUP_WATCH;
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
{
|
|
||||||
input: "js/plugin/main.ts",
|
|
||||||
output: {
|
|
||||||
file: "custom_components/browser_mod/browser_mod.js",
|
|
||||||
format: "es",
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve(),
|
|
||||||
json(),
|
|
||||||
typescript(),
|
|
||||||
babel({
|
|
||||||
exclude: "node_modules/**",
|
|
||||||
}),
|
|
||||||
!dev && terser({ format: { comments: false } }),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "js/config_panel/main.ts",
|
|
||||||
output: {
|
|
||||||
file: "custom_components/browser_mod/browser_mod_panel.js",
|
|
||||||
format: "es",
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve(),
|
|
||||||
json(),
|
|
||||||
typescript(),
|
|
||||||
babel({
|
|
||||||
exclude: "node_modules/**",
|
|
||||||
}),
|
|
||||||
!dev && terser({ format: { comments: false } }),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,25 +0,0 @@
|
|||||||
- 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%}"
|
|
@ -1,62 +0,0 @@
|
|||||||
default_config:
|
|
||||||
|
|
||||||
automation: !include test/automations.yaml
|
|
||||||
|
|
||||||
demo:
|
|
||||||
|
|
||||||
http:
|
|
||||||
use_x_forwarded_for: true
|
|
||||||
trusted_proxies:
|
|
||||||
- 172.17.0.0/24
|
|
||||||
|
|
||||||
logger:
|
|
||||||
default: warning
|
|
||||||
logs:
|
|
||||||
custom_components.browser_mod: info
|
|
||||||
|
|
||||||
# debugpy:
|
|
||||||
|
|
||||||
# browser_mod:
|
|
||||||
# devices:
|
|
||||||
# camdevice:
|
|
||||||
# camera: true
|
|
||||||
# testdevice:
|
|
||||||
# alias: test
|
|
||||||
# fully:
|
|
||||||
# force_stay_awake: true
|
|
||||||
# fully2:
|
|
||||||
# screensaver: true
|
|
||||||
|
|
||||||
lovelace:
|
|
||||||
mode: storage
|
|
||||||
dashboards:
|
|
||||||
lovelace-yaml:
|
|
||||||
mode: yaml
|
|
||||||
title: yaml
|
|
||||||
filename: test/lovelace.yaml
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
themes:
|
|
||||||
red:
|
|
||||||
primary-color: red
|
|
||||||
test:
|
|
||||||
card-mod-theme: test
|
|
||||||
card-mod-more-info-yaml: |
|
|
||||||
$: |
|
|
||||||
.mdc-dialog {
|
|
||||||
backdrop-filter: grayscale(0.7) blur(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
tts:
|
|
||||||
- platform: google_translate
|
|
||||||
# base_url: !env_var OUT_ADDR
|
|
||||||
|
|
||||||
script:
|
|
||||||
cm_debug:
|
|
||||||
sequence:
|
|
||||||
- service: browser_mod.debug
|
|
||||||
print_id:
|
|
||||||
sequence:
|
|
||||||
- service: system_log.write
|
|
||||||
data:
|
|
||||||
message: "Button was clicked in {{browser_id}}."
|
|
@ -1,17 +0,0 @@
|
|||||||
version: "3.0"
|
|
||||||
|
|
||||||
services:
|
|
||||||
test:
|
|
||||||
image: thomasloven/hass-custom-devcontainer
|
|
||||||
environment:
|
|
||||||
- HASS_USERNAME
|
|
||||||
- HASS_PASSWORD
|
|
||||||
- LOVELACE_LOCAL_FILES
|
|
||||||
- LOVELACE_PLUGINS
|
|
||||||
volumes:
|
|
||||||
- ./configuration.yaml:/config/configuration.yaml:ro
|
|
||||||
- .:/config/test:ro
|
|
||||||
- ..:/config/www/workspace
|
|
||||||
- ../custom_components:/config/custom_components
|
|
||||||
ports:
|
|
||||||
- 8125:8123
|
|
@ -1,67 +0,0 @@
|
|||||||
name: browser_mod
|
|
||||||
popup_cards:
|
|
||||||
sun.sun:
|
|
||||||
title: Global Popup-card
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Gobal popup for sun.sun
|
|
||||||
views:
|
|
||||||
- title: Player
|
|
||||||
cards:
|
|
||||||
- type: custom:browser-player
|
|
||||||
- type: picture-elements
|
|
||||||
image: https://placekitten.com/g/800/600
|
|
||||||
elements:
|
|
||||||
- type: state-icon
|
|
||||||
entity: light.bed_light
|
|
||||||
style:
|
|
||||||
top: 50%
|
|
||||||
left: 50%
|
|
||||||
transform: none
|
|
||||||
animation: spin 4s linear infinite
|
|
||||||
style: |
|
|
||||||
@keyframes spin {
|
|
||||||
100% {
|
|
||||||
transform: rotate(359deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
- type: button
|
|
||||||
entity: sun.sun
|
|
||||||
name: Global popup-card
|
|
||||||
tap_action:
|
|
||||||
action: more-info
|
|
||||||
|
|
||||||
- !include views/popup.yaml
|
|
||||||
- !include views/frontend-backend.yaml
|
|
||||||
|
|
||||||
- title: Popup card
|
|
||||||
popup_cards:
|
|
||||||
sun.sun:
|
|
||||||
title: Local Popup-card
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Local popup for sun.sun
|
|
||||||
climate.hvac:
|
|
||||||
title: Local Popup-card
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Local popup for climate.hvac
|
|
||||||
light.kitchen_lights:
|
|
||||||
type: No card
|
|
||||||
|
|
||||||
cards:
|
|
||||||
- type: button
|
|
||||||
entity: climate.hvac
|
|
||||||
name: Local popup-card
|
|
||||||
tap_action:
|
|
||||||
action: more-info
|
|
||||||
- type: button
|
|
||||||
entity: sun.sun
|
|
||||||
name: Override global popup-card
|
|
||||||
tap_action:
|
|
||||||
action: more-info
|
|
||||||
- type: button
|
|
||||||
entity: light.kitchen_lights
|
|
||||||
name: No card config
|
|
||||||
tap_action:
|
|
||||||
action: more-info
|
|
@ -1,30 +0,0 @@
|
|||||||
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')}}"
|
|
@ -1,225 +0,0 @@
|
|||||||
x-anchors:
|
|
||||||
default: &default
|
|
||||||
type: button
|
|
||||||
icon: mdi:star
|
|
||||||
|
|
||||||
desc: &desc
|
|
||||||
type: markdown
|
|
||||||
style: |
|
|
||||||
code {
|
|
||||||
font-size: 8pt;
|
|
||||||
line-height: normal;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
title: Popup
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
## Service: `browser_mod.popup`
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
```
|
|
||||||
title: Default
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
```
|
|
||||||
- <<: *default
|
|
||||||
name: Default
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Default
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
```
|
|
||||||
title: Large
|
|
||||||
large: true
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
```
|
|
||||||
- <<: *default
|
|
||||||
name: Large
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Large
|
|
||||||
large: true
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
```
|
|
||||||
title: Hide Header
|
|
||||||
hide_header: true
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
```
|
|
||||||
- <<: *default
|
|
||||||
name: Hide header
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Hide Header
|
|
||||||
hide_header: true
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
```
|
|
||||||
title: Auto close
|
|
||||||
auto_close: true
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
```
|
|
||||||
- <<: *default
|
|
||||||
name: Auto close
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Auto close
|
|
||||||
auto_close: true
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
```
|
|
||||||
|
|
||||||
title: Popup 1
|
|
||||||
card:
|
|
||||||
<<: *default
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Popup 2
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
```
|
|
||||||
- <<: *default
|
|
||||||
name: Nested popup
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Popup 1
|
|
||||||
card:
|
|
||||||
<<: *default
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Popup 2
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
More info in popup.
|
|
||||||
May have different behavior depending on whether a more-info dialog
|
|
||||||
has ever been opened before a popup.
|
|
||||||
- type: entities
|
|
||||||
entities: [light.bed_light]
|
|
||||||
- <<: *default
|
|
||||||
name: More info in popup
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: More info in popup
|
|
||||||
card:
|
|
||||||
type: entities
|
|
||||||
entities: [light.bed_light]
|
|
||||||
|
|
||||||
- type: vertical-stack
|
|
||||||
cards:
|
|
||||||
- <<: *desc
|
|
||||||
content: |
|
|
||||||
```
|
|
||||||
style:
|
|
||||||
$: |
|
|
||||||
.mdc-dialog {
|
|
||||||
backdrop-filter: blur(17px);
|
|
||||||
-webkit-backdrop-filter: blur(17px);
|
|
||||||
}
|
|
||||||
.mdc-dialog .mdc-dialog__container .mdc-dialog__surface {
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
.: |
|
|
||||||
:host {
|
|
||||||
--mdc-theme-surface: rgba(0,0,0,0);
|
|
||||||
--secondary-background-color: rgba(0,0,0,0.5);
|
|
||||||
--ha-card-background: rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
:host .content {
|
|
||||||
width: 90vw;
|
|
||||||
height: 90vh;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- <<: *default
|
|
||||||
name: Styled
|
|
||||||
tap_action:
|
|
||||||
action: fire-dom-event
|
|
||||||
browser_mod:
|
|
||||||
command: popup
|
|
||||||
title: Styled popup
|
|
||||||
card:
|
|
||||||
type: markdown
|
|
||||||
content: Popup!
|
|
||||||
style:
|
|
||||||
$: |
|
|
||||||
.mdc-dialog {
|
|
||||||
backdrop-filter: blur(17px);
|
|
||||||
-webkit-backdrop-filter: blur(17px);
|
|
||||||
}
|
|
||||||
.mdc-dialog .mdc-dialog__container .mdc-dialog__surface {
|
|
||||||
border-radius: 25px;
|
|
||||||
}
|
|
||||||
.: |
|
|
||||||
:host {
|
|
||||||
--mdc-theme-surface: rgba(0,0,0,0);
|
|
||||||
--secondary-background-color: rgba(0,0,0,0.5);
|
|
||||||
--ha-card-background: rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
:host .content {
|
|
||||||
width: 90vw;
|
|
||||||
height: 90vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
- <<: *default
|
|
||||||
name: Close popup
|
|
||||||
tap_action:
|
|
||||||
action: call-service
|
|
||||||
service: browser_mod.close_popup
|
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2017",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"experimentalDecorators": true
|
|
||||||
}
|
|
||||||
}
|
|
10
webpack.config.js
Normal file
10
webpack.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './js/main.js',
|
||||||
|
mode: 'production',
|
||||||
|
output: {
|
||||||
|
filename: 'custom_components/browser_mod/browser_mod.js',
|
||||||
|
path: path.resolve(__dirname)
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user