Fork sync

This commit is contained in:
SBado 2020-10-26 09:09:25 +01:00
commit c6b6a0c5d9
26 changed files with 792 additions and 468 deletions

45
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,45 @@
---
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 ammount of code possible to reproduce my error
# End of code
```
Error messages from the browser console:
---
**By putting an X in the boxes ([ ]) 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.

View File

@ -0,0 +1,7 @@
---
name: Feature request
about: For suggesting new features
title: ''
labels: 'feature-request'
assignees: ''
---

9
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,9 @@
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

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
node_modules/ node_modules/
**/__pycache__/ **/__pycache__/
.vscode
.env

View File

@ -72,10 +72,14 @@ This binds the *aliases* `arrakis` to `99980b13-dabc9563` and `dashboard` to `d2
Note: Aliases must be unique. Note: Aliases must be unique.
#### Experimental: Custom deviceID #### Changing deviceID
You can change the deviceID of your device by adding a `browser-player` card to your lovelace interface and clicking the deviceID at the bottom of the card. Set it to `clear` to generate a new random one.
You can also set a deviceID by adding `?deviceID=mydeviceID` to the end of the URL you're using to access Home Assistant. Be careful - I have no idea what could happen if several devices were to have the same ID. You can also set a deviceID by adding `?deviceID=mydeviceID` to the end of the URL you're using to access Home Assistant. Be careful - I have no idea what could happen if several devices were to have the same ID.
Use `?deviceID=clear` to generate a new random one. Use `?deviceID=clear` to generate a new random one.
**Take care to avoid deviceID collissions. There's no telling what could happen if more devices share the same ID.**
### Prefix ### Prefix
You can add a custom prefix to all entity ids in `configuration.yaml`: You can add a custom prefix to all entity ids in `configuration.yaml`:
@ -137,6 +141,8 @@ The sensor also has the following attributes:
| `fullyKiosk` | True if the *device* is a Fully Kiosk browser. Undefined otherwise. | | `fullyKiosk` | True if the *device* is a Fully Kiosk browser. Undefined otherwise. |
| `width` | The current width of the browser window in pixels. | | `width` | The current width of the browser window in pixels. |
| `height` | The current height of the browser window in pixels. | | `height` | The current height of the browser window in pixels. |
| `battery_level` | The current battery level of your device - if supported |
| `charging` | The current charging state of your device - if supported |
### media\_player ### media\_player
@ -150,7 +156,7 @@ The `light` can be used to blackout the screen.
For Fully Kiosk Browser, the screen will actually turn off. 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). 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) ### camera
For security and UX reasons, the camera must be enabled manually on a device by device basis. For security and UX reasons, the camera must be enabled manually on a device by device basis.
@ -179,7 +185,7 @@ It's state will be the state of the camera motion detector of the *device* (5 se
`browser_mod` registers a number of 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. 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** currently 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. 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. Alternatively, `deviceID: this` will also work.
@ -333,7 +339,7 @@ Second, there are a few more attributes available
| --- | --- | | --- | --- |
| `fullyKiosk` | True. | | `fullyKiosk` | True. |
| `brightness` | The current screen brightness. | | `brightness` | The current screen brightness. |
| `battery` | The current charge percentage of the devices battery. | | `battery_level` | The current charge percentage of the devices battery. |
| `charging` | Whether the battery is currently charging. | | `charging` | Whether the battery is currently charging. |
| `motion` | Whether the devices camera has detected any motion in the last five seconds. | | `motion` | Whether the devices camera has detected any motion in the last five seconds. |

View File

@ -3,39 +3,50 @@ import logging
from .mod_view import setup_view from .mod_view import setup_view
from .connection import setup_connection from .connection import setup_connection
from .service import setup_service from .service import setup_service
from .const import DOMAIN, DATA_DEVICES, DATA_ALIASES, DATA_ADDERS, CONFIG_DEVICES, DATA_CONFIG from .const import (
DOMAIN,
DATA_DEVICES,
DATA_ALIASES,
DATA_ADDERS,
CONFIG_DEVICES,
DATA_CONFIG,
DATA_SETUP_COMPLETE,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
await setup_connection(hass, config) async def async_setup(hass, config):
setup_view(hass)
aliases = {} aliases = {}
for d in config[DOMAIN].get(CONFIG_DEVICES, {}): for d in config[DOMAIN].get(CONFIG_DEVICES, {}):
name = config[DOMAIN][CONFIG_DEVICES][d].get("name", None) name = config[DOMAIN][CONFIG_DEVICES][d].get("name", None)
if name: if name:
aliases[name] = d.replace('_','-') aliases[name] = d.replace('_', '-')
hass.data[DOMAIN] = { hass.data[DOMAIN] = {
DATA_DEVICES: {}, DATA_DEVICES: {},
DATA_ALIASES: aliases, DATA_ALIASES: aliases,
DATA_ADDERS: {}, DATA_ADDERS: {},
DATA_CONFIG: config[DOMAIN], DATA_CONFIG: config[DOMAIN],
DATA_SETUP_COMPLETE: False,
} }
await hass.helpers.discovery.async_load_platform("media_player", DOMAIN, {}, config) await setup_connection(hass, config)
await hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) setup_view(hass)
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)
async_load_platform = hass.helpers.discovery.async_load_platform
await async_load_platform("media_player", DOMAIN, {}, config)
await async_load_platform("sensor", DOMAIN, {}, config)
await async_load_platform("binary_sensor", DOMAIN, {}, config)
await async_load_platform("light", DOMAIN, {}, config)
await async_load_platform("camera", DOMAIN, {}, config)
await setup_service(hass) await setup_service(hass)
hass.data[DOMAIN][DATA_SETUP_COMPLETE] = True
for device in hass.data[DOMAIN][DATA_DEVICES].values():
device.trigger_update()
return True return True

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ class BrowserModCamera(Camera, BrowserModEntity):
self.last_seen = None self.last_seen = None
def updated(self): def updated(self):
if self.last_seen is None or (datetime.now() - self.last_seen).seconds > 15:
self.last_seen = datetime.now() self.last_seen = datetime.now()
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -1,14 +1,19 @@
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.websocket_api import websocket_command, result_message, event_message, async_register_command from homeassistant.components.websocket_api import (
from homeassistant.helpers.entity import Entity, async_generate_entity_id websocket_command,
result_message,
event_message,
async_register_command
)
from .const import DOMAIN, WS_CONNECT, WS_UPDATE, WS_CAMERA from .const import WS_CONNECT, WS_UPDATE
from .helpers import get_devices, create_entity, get_config from .helpers import get_devices, create_entity, get_config, is_setup_complete
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def setup_connection(hass, config): async def setup_connection(hass, config):
@websocket_command({ @websocket_command({
@ -18,7 +23,8 @@ async def setup_connection(hass, config):
def handle_connect(hass, connection, msg): def handle_connect(hass, connection, msg):
deviceID = msg["deviceID"] deviceID = msg["deviceID"]
device = get_devices(hass).get(deviceID, BrowserModConnection(hass, deviceID)) device = get_devices(hass).get(deviceID,
BrowserModConnection(hass, deviceID))
device.connect(connection, msg["id"]) device.connect(connection, msg["id"])
get_devices(hass)[deviceID] = device get_devices(hass)[deviceID] = device
@ -29,7 +35,7 @@ async def setup_connection(hass, config):
vol.Required("deviceID"): str, vol.Required("deviceID"): str,
vol.Optional("data"): dict, vol.Optional("data"): dict,
}) })
def handle_update( hass, connection, msg): def handle_update(hass, connection, msg):
devices = get_devices(hass) devices = get_devices(hass)
deviceID = msg["deviceID"] deviceID = msg["deviceID"]
if deviceID in devices: if deviceID in devices:
@ -38,6 +44,7 @@ async def setup_connection(hass, config):
async_register_command(hass, handle_connect) async_register_command(hass, handle_connect)
async_register_command(hass, handle_update) async_register_command(hass, handle_update)
class BrowserModConnection: class BrowserModConnection:
def __init__(self, hass, deviceID): def __init__(self, hass, deviceID):
self.hass = hass self.hass = hass
@ -52,7 +59,7 @@ class BrowserModConnection:
def connect(self, connection, cid): def connect(self, connection, cid):
self.connection.append((connection, cid)) self.connection.append((connection, cid))
self.send("update", **get_config(self.hass, self.deviceID)) self.trigger_update()
def disconnect(): def disconnect():
self.connection.remove((connection, cid)) self.connection.remove((connection, cid))
@ -67,6 +74,10 @@ class BrowserModConnection:
**kwargs, **kwargs,
})) }))
def trigger_update(self):
if is_setup_complete(self.hass):
self.send("update", **get_config(self.hass, self.deviceID))
def update(self, data): def update(self, data):
if data.get('browser'): if data.get('browser'):
self.sensor = self.sensor or create_entity( self.sensor = self.sensor or create_entity(
@ -112,4 +123,3 @@ class BrowserModConnection:
self) self)
if self.camera: if self.camera:
self.camera.data = data.get('camera') self.camera.data = data.get('camera')

View File

@ -8,6 +8,7 @@ DATA_DEVICES = "devices"
DATA_ALIASES = "aliases" DATA_ALIASES = "aliases"
DATA_ADDERS = "adders" DATA_ADDERS = "adders"
DATA_CONFIG = "config" DATA_CONFIG = "config"
DATA_SETUP_COMPLETE = "setup_complete"
CONFIG_DEVICES = "devices" CONFIG_DEVICES = "devices"
CONFIG_PREFIX = "prefix" CONFIG_PREFIX = "prefix"

View File

@ -2,35 +2,53 @@ import logging
from homeassistant.helpers.entity import Entity, async_generate_entity_id 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 from .const import (
DOMAIN,
DATA_DEVICES,
DATA_ALIASES,
DATA_ADDERS,
CONFIG_DEVICES,
DATA_CONFIG,
CONFIG_PREFIX,
CONFIG_DISABLE,
CONFIG_DISABLE_ALL,
DATA_SETUP_COMPLETE,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def get_devices(hass): def get_devices(hass):
return hass.data[DOMAIN][DATA_DEVICES] return hass.data[DOMAIN][DATA_DEVICES]
def get_alias(hass, deviceID): def get_alias(hass, deviceID):
for k,v in hass.data[DOMAIN][DATA_ALIASES].items(): for k, v in hass.data[DOMAIN][DATA_ALIASES].items():
if v == deviceID: if v == deviceID:
return k return k
return None return None
def get_config(hass, deviceID): def get_config(hass, deviceID):
config = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DEVICES, {}) config = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DEVICES, {})
return config.get(deviceID, config.get(deviceID.replace('-','_'), {})) return config.get(deviceID, config.get(deviceID.replace('-', '_'), {}))
def create_entity(hass, platform, deviceID, connection): def create_entity(hass, platform, deviceID, connection):
conf = get_config(hass, deviceID) conf = get_config(hass, deviceID)
if conf and (platform in conf.get(CONFIG_DISABLE, []) if conf and (platform in conf.get(CONFIG_DISABLE, [])
or CONFIG_DISABLE_ALL in conf.get(CONFIG_DISABLE, [])): or CONFIG_DISABLE_ALL in conf.get(CONFIG_DISABLE, [])):
return None return None
if not conf and (platform in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, []) if not conf and \
or CONFIG_DISABLE_ALL in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, [])): (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 return None
adder = hass.data[DOMAIN][DATA_ADDERS][platform] adder = hass.data[DOMAIN][DATA_ADDERS][platform]
entity = adder(hass, deviceID, connection, get_alias(hass, deviceID)) entity = adder(hass, deviceID, connection, get_alias(hass, deviceID))
return entity return entity
def setup_platform(hass, config, async_add_devices, platform, cls): def setup_platform(hass, config, async_add_devices, platform, cls):
def adder(hass, deviceID, connection, alias=None): def adder(hass, deviceID, connection, alias=None):
entity = cls(hass, connection, deviceID, alias) entity = cls(hass, connection, deviceID, alias)
@ -39,6 +57,11 @@ def setup_platform(hass, config, async_add_devices, platform, cls):
hass.data[DOMAIN][DATA_ADDERS][platform] = adder hass.data[DOMAIN][DATA_ADDERS][platform] = adder
return True return True
def is_setup_complete(hass):
return hass.data[DOMAIN][DATA_SETUP_COMPLETE]
class BrowserModEntity(Entity): class BrowserModEntity(Entity):
def __init__(self, hass, connection, deviceID, alias=None): def __init__(self, hass, connection, deviceID, alias=None):
@ -47,7 +70,11 @@ class BrowserModEntity(Entity):
self.deviceID = deviceID self.deviceID = deviceID
self._data = {} self._data = {}
prefix = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_PREFIX, '') 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) self.entity_id = async_generate_entity_id(
self.domain+".{}",
alias or f"{prefix}{deviceID}",
hass=hass
)
def updated(self): def updated(self):
pass pass
@ -55,6 +82,7 @@ class BrowserModEntity(Entity):
@property @property
def data(self): def data(self):
return self._data return self._data
@data.setter @data.setter
def data(self, data): def data(self, data):
self._data = data self._data = data

View File

@ -48,6 +48,10 @@ class BrowserModLight(LightEntity, BrowserModEntity):
return SUPPORT_BRIGHTNESS return SUPPORT_BRIGHTNESS
return 0 return 0
@property
def brightness(self):
return self.data.get('brightness', None)
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
self.connection.send("no-blackout", **kwargs) self.connection.send("no-blackout", **kwargs)

View File

@ -1,9 +1,85 @@
command: command:
description: Send a command to a browser description: 'Send a command to a browser.'
fields: fields:
command: command:
description: Command to send description: 'Command to send'
example: 'navigate' example: 'navigate'
deviceID: deviceID:
description: List of receiving browsers description: 'List of receiving browsers'
example: '["99980b13-dabc9563", "office_computer"]' example: '["99980b13-dabc9563", "office_computer"]'
debug:
description: 'On all browsers, show a popup, and a javascript alert with the current device ID.'
set_theme:
description: 'On all browsers, change the theme.'
fields:
theme:
description: 'Theme to change to'
example: '{theme: "clear_light"}'
navigate:
description: 'Navigate to a path on a browser.'
fields:
navigation_path:
description: 'Path to navigate to'
example: '/lovelace/1'
deviceID:
description: 'List of receiving browsers'
example: '["99980b13-dabc9563", "office_computer"]'
more_info:
description: 'Open the more info dialog of an entity on a browser.'
fields:
entity_id:
description: 'Entity to show more info for'
example: 'camera.front_door'
deviceID:
description: 'List of receiving browsers'
example: '["99980b13-dabc9563", "office_computer"]'
large:
description: '(optional) Set to true to make wider'
example: 'true'
toast:
description: 'Show a toast message in the bottom left on all browsers.'
fields:
message:
description: 'Message to show'
example: 'Short message'
duration:
description: '(optional) Time in milliseconds to show message for. Set to 0 for persistent display.'
example: '10000'
popup:
description: 'Pop up a card on a browser.'
fields:
title:
description: 'Name to show in popup bar'
example: 'Popup example'
card:
description: 'YAML config for card to show'
deviceID:
description: 'List of receiving browsers'
example: '["99980b13-dabc9563", "office_computer"]'
large:
description: '(optional) Set to true to make wider'
example: 'true'
hide_header:
description: '(optional) Hide header title and close button'
example: 'true'
auto_close:
description: '(optional) Close popup when mouse is moved or key is pressed. Also hides header'
example: 'true'
time:
description: "(optional) When mouse isn't moved or keys aren't pressed for this amount of seconds, reopen. Only usable with auto_close. See blackout"
example: '20'
close_popup:
description: 'Close all popups on all browsers.'
blackout:
description: 'Cover screen in black until the mouse is moved or a key is pressed.'
fields:
time:
description: '(optional) The blackout will turn on automatically after the specified number of seconds. It works kind of like a screensaver and will keep turning on until blackout is called again with time: -1.'
example: '20'
no_blackout:
description: 'Remove a blackout from a browser.'
fields:
brightness:
description: '(optional) On a Fully Kiosk Browser Plus set the screen brightness from 0 - 255.'
lovelace_reload:
description: 'Refresh the lovelace configuration.'

View File

@ -1,5 +1,4 @@
import { LitElement, html, css } from "card-tools/src/lit-element"; import { deviceID, setDeviceID } from "card-tools/src/deviceId"
import { deviceID } from "card-tools/src/deviceID"
import { moreInfo } from "card-tools/src/more-info" import { moreInfo } from "card-tools/src/more-info"
import "./browser-player-editor.js" import "./browser-player-editor.js"
@ -29,22 +28,31 @@ class BrowserPlayer extends LitElement {
setConfig(config) { setConfig(config) {
this._config = config; this._config = config;
for (const event of ["play", "pause", "ended", "volumechange", "canplay", "loadeddata"])
window.browser_mod.player.addEventListener(event, () => this.requestUpdate());
} }
handleMute(ev) { handleMute(ev) {
window.browser_mod.mute({}); window.browser_mod.player_mute();
} }
handleVolumeChange(ev) { handleVolumeChange(ev) {
const vol = parseFloat(ev.target.value); const vol = parseFloat(ev.target.value);
window.browser_mod.set_volume({volume_level: vol}); window.browser_mod.player_set_volume(vol);
} }
handleMoreInfo(ev) { handleMoreInfo(ev) {
moreInfo("media_player."+window.browser_mod.entity_id); moreInfo("media_player."+window.browser_mod.entity_id);
} }
handlePlayPause(ev) { handlePlayPause(ev) {
if (window.browser_mod.player.paused) if (window.browser_mod.player.paused)
window.browser_mod.play({}); window.browser_mod.player_play();
else else
window.browser_mod.pause({}); window.browser_mod.player_pause();
}
setDeviceID() {
const newID = prompt("Set deviceID", deviceID);
if (newID !== deviceID) {
setDeviceID(newID);
this.requestUpdate();
}
} }
render() { render() {
@ -63,34 +71,34 @@ class BrowserPlayer extends LitElement {
} }
@click=${this.handleMute} @click=${this.handleMute}
></ha-icon-button> ></ha-icon-button>
<ha-paper-slider <ha-slider
min=0 min=0
max=1 max=1
step=0.01 step=0.01
?disabled=${player.muted} ?disabled=${player.muted}
value=${player.volume} value=${player.volume}
@change=${this.handleVolumeChange} @change=${this.handleVolumeChange}
></ha-paper-slider> ></ha-slider>
${window.browser_mod.player_state === "stopped" ${window.browser_mod.player_state === "stopped"
? html`<div class="placeholder"></div>` ? html`<div class="placeholder"></div>`
: html` : html`
<paper-icon-button <ha-icon-button
.icon=${player.paused .icon=${player.paused
? "mdi:play" ? "mdi:play"
: "mdi:pause" : "mdi:pause"
} }
@click=${this.handlePlayPause} @click=${this.handlePlayPause}
highlight highlight
></paper-icon-button> ></ha-icon-button>
`} `}
<paper-icon-button <ha-icon-button
.icon=${"mdi:settings"} .icon=${"mdi:cog"}
@click=${this.handleMoreInfo} @click=${this.handleMoreInfo}
></paper-icon-button> ></ha-icon-button>
</div> </div>
<div class="device-id"> <div class="device-id" @click=${this.setDeviceID}>
${deviceID} ${deviceID}
</div> </div>

35
js/browser.js Normal file
View File

@ -0,0 +1,35 @@
import { fireEvent } from "card-tools/src/event";
export const BrowserModBrowserMixin = (C) => class extends C {
constructor() {
super();
document.addEventListener("visibilitychange", () => this.sensor_update());
window.addEventListener("location-changed", () => this.sensor_update());
window.setInterval(() => this.sensor_update(), 10000);
}
sensor_update() {
window.queueMicrotask( async () => {
const battery = navigator.getBattery ? await navigator.getBattery() : undefined;
this.sendUpdate({browser: {
path: window.location.pathname,
visibility: document.visibilityState,
userAgent: navigator.userAgent,
currentUser: this._hass &&this._hass.user && this._hass.user.name,
fullyKiosk: this.isFully,
width: window.innerWidth,
height: window.innerHeight,
battery_level: this.isFully ? window.fully.getBatteryLevel() : battery ? battery.level*100 : undefined,
charging: this.isFully ? window.fully.isPlugged() : battery ? battery.charging : undefined,
}});
});
}
do_navigate(path) {
if (!path) return;
history.pushState(null, "", path);
fireEvent("location-changed", {}, document.querySelector("home-assistant"));
}
}

46
js/camera.js Normal file
View File

@ -0,0 +1,46 @@
export const BrowserModCameraMixin = (C) => class extends C {
setup_camera() {
console.log("Starting camera")
if(this._video) return;
this._video = document.createElement("video");
this._video.autoplay = true;
this._video.playsInline = true;
this._video.style.display = "none";
this._canvas = document.createElement("canvas");
this._canvas.style.display = "none";
document.body.appendChild(this._video);
document.body.appendChild(this._canvas);
if(!navigator.mediaDevices) return;
console.log("Starting devices")
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then((stream) => {
this._video.srcObject = stream;
this._video.play();
this.update_camera();
})
this._camera_framerate = 2;
window.addEventListener("click", () => this._video.play(), {once: true});
}
update_camera() {
this._canvas.width = this._video.videoWidth;
this._canvas.height = this._video.videoHeight;
const context = this._canvas.getContext('2d');
context.drawImage(this._video, 0, 0, this._video.videoWidth, this._video.videoHeight);
this.sendUpdate({
camera: this._canvas.toDataURL('image/jpeg'),
});
setTimeout(() => this.update_camera(), Math.round(1000 / this._camera_framerate));
}
}

71
js/connection.js Normal file
View File

@ -0,0 +1,71 @@
import { deviceID } from "card-tools/src/deviceId";
import { hass, provideHass } from "card-tools/src/hass";
export class BrowserModConnection{
async connect() {
const isCast = document.querySelector("hc-main") !== null;
if(!isCast) {
if(!window.hassConnection) {
window.setTimeout(() => this._do_connect(), 100);
return;
} else {
this._connection = (await window.hassConnection).conn;
}
} else {
this._connection = hass().connection;
}
this._connection.subscribeMessage((msg) => this.msg_callback(msg), {
type: 'browser_mod/connect',
deviceID: deviceID,
});
this._hass_patched = false;
provideHass(this);
}
set hass(hass) {
this._hass = hass;
if(!hass || this._hass_patched) return;
this._hass_patched = true;
const callService = hass.callService;
hass.callService = (domain, service, serviceData) => {
if(serviceData && serviceData.deviceID) {
serviceData = JSON.parse(JSON.stringify(serviceData));
const orig = JSON.stringify(serviceData.deviceID);
const patched = orig.replace('"this"', `"${deviceID}"`);
serviceData.deviceID = JSON.parse(patched);
}
return callService(domain, service, serviceData);
}
if (document.querySelector("hc-main"))
document.querySelector("hc-main").hassChanged(hass, hass);
else
document.querySelector("home-assistant").hassChanged(hass, hass);
}
get connected() {
return this._connection !== undefined;
}
msg_callback(message) {
console.log(message);
}
sendUpdate(data) {
if(!this.connected) return;
this._connection.sendMessage({
type: 'browser_mod/update',
deviceID,
data,
}
)
}
}

39
js/fullyKiosk.js Normal file
View File

@ -0,0 +1,39 @@
export const FullyKioskMixin = (C) => class extends C {
get isFully() {
return window.fully !== undefined;
}
constructor() {
super();
if (!this.isFully) return;
this._fullyMotion = false;
this._motionTimeout = undefined;
for (const event of ["screenOn", "screenOff", "pluggedAC", "pluggedUSB", "onBatteryLevelChanged", "unplugged", "networkReconnect"]) {
fully.bind(event, "window.browser_mod.fully_update();");
}
window.fully.bind("onMotion", "window.browser_mod.fullyMotionTriggered();");
}
fully_update() {
if(!this.isFully) return
this.sendUpdate({fully: {
battery: window.fully.getBatteryLevel(),
charging: window.fully.isPlugged(),
motion: this._fullyMotion,
}})
}
fullyMotionTriggered() {
this._fullyMotion = true;
clearTimeout(this._motionTimeout);
this._motionTimeout = setTimeout(() => {
this._fullyMotion = false;
this.fully_update();
}, 5000);
this.fully_update();
}
}

View File

@ -1,123 +1,36 @@
import { deviceID } from "card-tools/src/deviceID"; import { deviceID } from "card-tools/src/deviceID";
import { lovelace_view, provideHass, load_lovelace, lovelace, hass } from "card-tools/src/hass"; import { lovelace_view } from "card-tools/src/hass";
import { popUp, closePopUp } from "card-tools/src/popup"; import { popUp } from "card-tools/src/popup";
import { fireEvent } from "card-tools/src/event"; import { fireEvent } from "card-tools/src/event";
import { moreInfo } from "card-tools/src/more-info.js";
import "./browser-player"; import "./browser-player";
class BrowserMod { import { BrowserModConnection } from "./connection";
import { BrowserModMediaPlayerMixin } from "./mediaPlayer";
import { FullyKioskMixin } from "./fullyKiosk";
import { BrowserModCameraMixin } from "./camera";
import { BrowserModScreensaverMixin } from "./screensaver";
import { BrowserModPopupsMixin } from "./popups";
import { BrowserModBrowserMixin } from "./browser";
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 = JSON.parse(JSON.stringify(serviceData));
serviceData.deviceID[index] = deviceID;
}
} else if(serviceData.deviceID === "this") {
serviceData = JSON.parse(JSON.stringify(serviceData));
serviceData.deviceID = deviceID;
}
}
return callService(domain, service, serviceData);
};
hass.callService = newCallService;
this.hassPatched = true; const ext = (baseClass, mixins) =>
if(document.querySelector("hc-main")) mixins.reduceRight((base, mixin) => mixin(base), baseClass);
document.querySelector("hc-main").hassChanged(hass,hass);
else
document.querySelector("home-assistant").hassChanged(hass, hass);
}
playOnce(ev) { class BrowserMod extends ext(BrowserModConnection, [
if(this._video) this._video.play(); BrowserModBrowserMixin,
if(window.browser_mod.playedOnce) return; BrowserModPopupsMixin,
window.browser_mod.player.play(); BrowserModScreensaverMixin,
window.browser_mod.playedOnce = true; BrowserModCameraMixin,
} FullyKioskMixin,
BrowserModMediaPlayerMixin,
]) {
async _load_lovelace() {
if(!await load_lovelace()) {
let timer = window.setTimeout(this._load_lovelace.bind(this), 100);
}
}
_connect() {
if(!window.hassConnection) {
window.setTimeout(() => this._connect(), 100);
} else {
window.hassConnection.then((conn) => this.connect(conn.conn));
}
}
constructor() { constructor() {
this.entity_id = deviceID.replace("-","_"); super();
this.entity_id = deviceID.replace("-", "_");
this.cast = document.querySelector("hc-main") !== null; this.cast = document.querySelector("hc-main") !== null;
if(!this.cast) { this.connect();
window.setTimeout(this._load_lovelace.bind(this), 500);
this._connect();
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);
const pjson = require('../package.json'); const pjson = require('../package.json');
console.info(`%cBROWSER_MOD ${pjson.version} IS INSTALLED console.info(`%cBROWSER_MOD ${pjson.version} IS INSTALLED
@ -125,96 +38,37 @@ class BrowserMod {
"color: green; font-weight: bold", ""); "color: green; font-weight: bold", "");
} }
connect(conn) { msg_callback(msg) {
this.conn = conn const handlers = {
conn.subscribeMessage((msg) => this.callback(msg), { update: (msg) => this.update(msg),
type: 'browser_mod/connect', debug: (msg) => this.debug(msg),
deviceID: deviceID,
}); play: (msg) => this.player_play(msg.media_content_id),
pause: (msg) => this.player_pause(),
stop: (msg) => this.player_stop(),
set_volume: (msg) => this.player_set_volume(msg.volume_level),
mute: (msg) => this.player_mute(msg.mute),
toast: (msg) => this.do_toast(msg.message, msg.duration),
popup: (msg) => this.do_popup(msg),
"close-popup": (msg) => this.do_close_popup(),
"more-info": (msg) => this.do_more_info(msg.entity_id, msg.large),
navigate: (msg) => this.do_navigate(msg.navigation_path),
"set-theme": (msg) => this.set_theme(msg),
"lovelace-reload": (msg) => this.lovelace_reload(msg),
"window-reload": () => window.location.reload(false),
blackout: (msg) => this.do_blackout(msg.time ? parseInt(msg.time) : undefined),
"no-blackout": (msg) => {
if(msg.brightness && this.isFully) {
window.fully.setScreenBrightness(msg.brightness);
} }
this.no_blackout()
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 "window-reload":
window.location.reload(false);
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) {
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; handlers[msg.command](msg);
const d = data[ev.detail.entityId];
if(!d) return;
window.setTimeout(() => {
fireEvent("hass-more-info", {entityId: "."}, document.querySelector("home-assistant"));
popUp(d.title, d.card, d.large || false, d.style);
}, 50);
} }
debug(msg) { debug(msg) {
@ -222,90 +76,9 @@ class BrowserMod {
alert(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||msg.hide_header);
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){ set_theme(msg){
if(!msg.theme) msg.theme = "default"; if(!msg.theme) msg.theme = "default";
fireEvent("settheme", msg.theme, document.querySelector("home-assistant")); fireEvent("settheme", {theme: msg.theme}, document.querySelector("home-assistant"));
} }
lovelace_reload(msg) { lovelace_reload(msg) {
@ -314,145 +87,25 @@ class BrowserMod {
fireEvent("config-refresh", {}, 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) { update(msg=null) {
if(!this.conn) return;
if(msg) { if(msg) {
if(msg.name) { if(msg.name) {
this.entity_id = msg.name.toLowerCase(); this.entity_id = msg.name.toLowerCase();
} }
if(msg.camera) { if(msg.camera) {
this.start_camera(); this.setup_camera();
} }
} }
this.player_update();
this.conn.sendMessage({ this.fully_update();
type: 'browser_mod/update', this.screen_update();
deviceID: deviceID, this.sensor_update();
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,
},
});
} }
} }
const bases = [customElements.whenDefined('home-assistant-main'), customElements.whenDefined('hui-view')]; const bases = [customElements.whenDefined('home-assistant'), customElements.whenDefined('hc-main')];
Promise.race(bases).then(() => { Promise.race(bases).then(() => {
window.browser_mod = window.browser_mod || new BrowserMod(); window.browser_mod = window.browser_mod || new BrowserMod();
}); });

51
js/mediaPlayer.js Normal file
View File

@ -0,0 +1,51 @@
export const BrowserModMediaPlayerMixin = (C) => class extends C {
constructor() {
super();
this.player = new Audio();
for (const event of ["play", "pause", "ended", "volumechange"]) {
this.player.addEventListener(event, () => this.player_update());
}
window.addEventListener("click", () => this.player.play(), {once: true});
}
player_update(ev) {
this.sendUpdate({player: {
volume: this.player.volume,
muted: this.player.muted,
src: this.player.src,
state: this.player_state,
}})
}
get player_state() {
if (!this.player.src) return "stopped";
if (this.player.ended) return "stopped";
if (this.player.paused) return "paused";
return "playing";
}
player_play(src) {
if(src)
this.player.src = src;
this.player.play();
}
player_pause() {
this.player.pause();
}
player_stop() {
this.player.pause();
this.player.src = null;
}
player_set_volume(level) {
if(level === undefined) return;
this.player.volume = level;
}
player_mute(mute) {
if(mute === undefined)
mute = !this.player.muted;
this.player.muted = Boolean(mute);
}
}

79
js/popups.js Normal file
View File

@ -0,0 +1,79 @@
import { fireEvent } from "card-tools/src/event";
import { load_lovelace, lovelace } from "card-tools/src/hass";
import { moreInfo } from "card-tools/src/more-info";
import { closePopUp, popUp } from "card-tools/src/popup";
export const BrowserModPopupsMixin = (C) => class extends C {
constructor() {
super();
if (document.querySelector("home-assistant"))
document.querySelector("home-assistant").addEventListener("hass-more-info", (ev) => this._popup_card(ev));
const isCast = document.querySelector("hc-main") !== null;
if(!isCast)
load_lovelace();
}
_popup_card(ev) {
if(!lovelace()) return;
if(!ev.detail || !ev.detail.entityId) return;
const data = {
...lovelace().config.popup_cards,
...lovelace().config.views[lovelace().current_view].popup_cards,
};
const d = data[ev.detail.entityId];
if(!d) return;
window.queueMicrotask(() => {
fireEvent("hass-more-info", {entityID: "."}, document.querySelector("home-assistant"));
popUp(
d.title,
d.card,
d.large || false,
d.style
);
});
}
do_popup(cfg) {
if (!(cfg.title || cfg.auto_close || cfg.hide_header)) return;
if (!cfg.card) return;
const open = () => {
popUp(
cfg.tile,
cfg.card,
cfg.large,
cfg.style,
cfg.auto_close || cfg.hide_header,
);
};
if(cfg.auto_close) {
this.screensaver_set(open, closePopUp, cfg.time);
} else {
open();
}
}
do_close_popup() {
this.screensaver_stop();
closePopUp();
}
do_more_info(entity_id, large) {
if (!entity_id) return;
moreInfo(entity_id, large);
}
do_toast(message, duration) {
if (!message) return;
fireEvent("hass-notification", {
message,
duration: parseInt(duration),
}, document.querySelector("home-assistant"));
}
}

123
js/screensaver.js Normal file
View File

@ -0,0 +1,123 @@
export const BrowserModScreensaverMixin = (C) => class extends C {
constructor() {
super();
this._blackout_panel = document.createElement("div");
this._screenSaver = undefined;
this._screenSaverTimer = undefined;
this._screenSaverTimeOut = 0;
this._screenSaver = {
fn: undefined,
clearfn: undefined,
timer: undefined,
timeout: undefined,
listeners : {},
active: false,
};
this._blackout_panel.style.cssText = `
position: fixed;
left: 0;
top: 0;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
background: black;
display: none;
`;
document.body.appendChild(this._blackout_panel);
if(this.isFully) {
window.fully.bind("screenOn", "window.browser_mod.screen_update();");
window.fully.bind("screenOff", "window.browser_mod.screen_update();");
}
}
screensaver_set(fn, clearfn, time) {
this._ss_clear();
this._screenSaver = {
fn,
clearfn,
timer: undefined,
timeout: time,
listeners: {},
active: false,
}
const l = () => this.screensaver_update();
for(const event of ["mousemove", "mousedown", "keydown", "touchstart"]) {
window.addEventListener(event, l);
this._screenSaver.listeners[event] = l;
}
this._screenSaver.timer = window.setTimeout(() => this._ss_run(), time*1000);
}
screensaver_update() {
if (this._screenSaver.active) {
this.screensaver_stop();
} else {
window.clearTimeout(this._screenSaver.timer);
this._screenSaver.timer = window.setTimeout(() => this._ss_run(), this._screenSaver.timeout*1000);
}
}
screensaver_stop() {
this._ss_clear();
this._screenSaver.active = false;
if(this._screenSaver.clearfn)
this._screenSaver.clearfn();
if(this._screenSaver.timeout) {
this.screensaver_set(
this._screenSaver.fn,
this._screenSaver.clearfn,
this._screenSaver.timeout,
);
}
}
_ss_clear() {
window.clearTimeout(this._screenSaverTimer);
for(const [k, v] of Object.entries(this._screenSaver.listeners)) {
window.removeEventListener(k, v);
}
}
_ss_run() {
this._screenSaver.active = true;
this._screenSaver.fn();
}
do_blackout(timeout) {
this.screensaver_set(
() => {
if(this.isFully)
window.fully.turnScreenOff();
else
this._blackout_panel.style.display = "block";
this.screen_update();
},
() => {
if(this._blackout_panel.style.display = "block")
this._blackout_panel.style.display = "none"
if(this.isFully && !window.fully.getScreenOn())
window.fully.turnScreenOn();
this.screen_update();
},
timeout || 0
);
}
no_blackout() {
this.screensaver_stop();
}
screen_update() {
this.sendUpdate({screen: {
blackout: this.isFully
? !window.fully.getScreenOn()
: Boolean(this._blackout_panel.style.display === "block"),
brightness: this.isFully ? window.fully.getScreenBrightness() : undefined,
}})
}
}

4
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "browser_mod", "name": "browser_mod",
"version": "1.1.6", "version": "1.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -656,7 +656,7 @@
"dev": true "dev": true
}, },
"card-tools": { "card-tools": {
"version": "github:thomasloven/lovelace-card-tools#6d5ae3800e4937aa424edc17108f43b964aecce7", "version": "github:thomasloven/lovelace-card-tools#1272cf67c56e8f576e24c13f510568d544ad5d0b",
"from": "github:thomasloven/lovelace-card-tools" "from": "github:thomasloven/lovelace-card-tools"
}, },
"chalk": { "chalk": {

View File

@ -1,7 +1,7 @@
{ {
"name": "browser_mod", "name": "browser_mod",
"private": true, "private": true,
"version": "1.1.6", "version": "1.2.0",
"description": "", "description": "",
"scripts": { "scripts": {
"build": "webpack", "build": "webpack",

View File

@ -3,6 +3,12 @@ default_config:
demo: demo:
browser_mod: browser_mod:
devices:
camdevice:
camera: true
testdevice:
alias: test
lovelace: lovelace:
mode: yaml mode: yaml
@ -17,6 +23,8 @@ lovelace:
frontend: frontend:
themes: themes:
red:
primary-color: red
test: test:
card-mod-theme: test card-mod-theme: test
card-mod-more-info-yaml: | card-mod-more-info-yaml: |
@ -24,3 +32,12 @@ frontend:
.mdc-dialog { .mdc-dialog {
backdrop-filter: grayscale(0.7) blur(5px); 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

View File

@ -52,6 +52,8 @@ services:
volumes: *x-files volumes: *x-files
ports: ports:
- "5001:8123" - "5001:8123"
environment:
OUT_ADDR: "http://${DOCKER_GATEWAY_HOST:-localhost}:5001"
command: *x-command command: *x-command
dev: dev: