Added camera functionality

This commit is contained in:
Thomas Lovén 2022-07-13 23:05:27 +00:00
parent 466a5eb5e7
commit e4a65f3077
18 changed files with 355 additions and 223 deletions

View File

@ -3,14 +3,7 @@ import logging
from .store import BrowserModStore from .store import BrowserModStore
from .mod_view import async_setup_view from .mod_view import async_setup_view
from .connection import async_setup_connection from .connection import async_setup_connection
from .const import ( from .const import DOMAIN, DATA_DEVICES, DATA_ADDERS, DATA_STORE
DOMAIN,
DATA_DEVICES,
DATA_ADDERS,
DATA_STORE
)
from .coordinator import Coordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,13 +28,9 @@ async def async_setup_entry(hass, config_entry):
await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
await hass.config_entries.async_forward_entry_setup(config_entry, "light") await hass.config_entries.async_forward_entry_setup(config_entry, "light")
await hass.config_entries.async_forward_entry_setup(config_entry, "media_player") await hass.config_entries.async_forward_entry_setup(config_entry, "media_player")
await hass.config_entries.async_forward_entry_setup(config_entry, "camera")
await async_setup_connection(hass) await async_setup_connection(hass)
await async_setup_view(hass) await async_setup_view(hass)
return True return True
for component in COMPONENTS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True

View File

@ -1,18 +1,20 @@
from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.binary_sensor import BinarySensorEntity
from .const import DOMAIN, DATA_ADDERS from .const import DOMAIN, DATA_ADDERS
from .helpers import BrowserModEntity2 from .helpers import BrowserModEntity
async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): async def async_setup_platform(
hass, config_entry, async_add_entities, discoveryInfo=None
):
hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities hass.data[DOMAIN][DATA_ADDERS]["binary_sensor"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserBinarySensor(BrowserModEntity2, BinarySensorEntity): class BrowserBinarySensor(BrowserModEntity, BinarySensorEntity):
def __init__(self, coordinator, deviceID, parameter, name): def __init__(self, coordinator, deviceID, parameter, name):
super().__init__(coordinator, deviceID, name) super().__init__(coordinator, deviceID, name)
self.parameter = parameter self.parameter = parameter

View File

@ -732,19 +732,28 @@ const ConnectionMixin = (SuperClass) => {
} }
})(); })();
} }
async _reregister(newData = {}) {
await this.connection.sendMessage({
type: "browser_mod/reregister",
deviceID: this.deviceID,
data: Object.assign(Object.assign({}, this.devices[this.deviceID]), newData),
});
}
get meta() { get meta() {
if (!this.registered) if (!this.registered)
return null; return null;
return this.devices[this.deviceID].meta; return this.devices[this.deviceID].meta;
} }
set meta(value) { set meta(value) {
(async () => { this._reregister({ meta: value });
await this.connection.sendMessage({ }
type: "browser_mod/reregister", get cameraEnabled() {
deviceID: this.deviceID, if (!this.registered)
data: Object.assign(Object.assign({}, this.devices[this.deviceID]), { meta: value }) return null;
}); return this.devices[this.deviceID].camera;
})(); }
set cameraEnabled(value) {
this._reregister({ camera: value });
} }
sendUpdate(data) { sendUpdate(data) {
if (!this.connected || !this.registered) if (!this.connected || !this.registered)
@ -923,6 +932,65 @@ const MediaPlayerMixin = (SuperClass) => {
}; };
}; };
const CameraMixin = (SuperClass) => {
return class CameraMixinClass extends SuperClass {
constructor() {
super();
this._framerate = 2;
window.addEventListener("pointerdown", () => {
this._setup_camera();
}, { once: true });
}
async _setup_camera() {
if (this._video)
return;
await this.connectionPromise;
if (!this.cameraEnabled)
return;
const video = (this._video = document.createElement("video"));
video.autoplay = true;
video.playsInline = true;
video.style.display = "none";
const canvas = (this._canvas = document.createElement("canvas"));
canvas.style.display = "none";
document.body.appendChild(video);
document.body.appendChild(canvas);
if (!navigator.mediaDevices)
return;
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
video.srcObject = stream;
video.play();
this.update_camera();
}
async update_camera() {
var _a;
if (!this.cameraEnabled) {
const stream = (_a = this._video) === null || _a === void 0 ? void 0 : _a.srcObject;
if (stream) {
stream.getTracks().forEach((t) => t.stop());
this._video.scrObject = undefined;
}
return;
}
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);
}
};
};
var name = "browser_mod"; var name = "browser_mod";
var version = "2.0.0b0"; var version = "2.0.0b0";
var description = ""; var description = "";
@ -970,7 +1038,7 @@ var pjson = {
// FullyKioskMixin, // FullyKioskMixin,
// BrowserModMediaPlayerMixin, // BrowserModMediaPlayerMixin,
// ]) { // ]) {
class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) { class BrowserMod extends CameraMixin(MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget)))) {
constructor() { constructor() {
super(); super();
this.entity_id = deviceID.replace("-", "_"); this.entity_id = deviceID.replace("-", "_");

View File

@ -90,6 +90,9 @@ loadDevTools().then(() => {
changeDeviceID(ev) { changeDeviceID(ev) {
window.browser_mod.deviceID = ev.target.value; window.browser_mod.deviceID = ev.target.value;
} }
toggleCameraEnabled() {
window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled;
}
unregister_device(ev) { unregister_device(ev) {
const deviceID = ev.currentTarget.deviceID; const deviceID = ev.currentTarget.deviceID;
if (deviceID === window.browser_mod.deviceID) if (deviceID === window.browser_mod.deviceID)
@ -104,7 +107,7 @@ loadDevTools().then(() => {
window.browser_mod.addEventListener("browser-mod-config-update", () => this.requestUpdate()); window.browser_mod.addEventListener("browser-mod-config-update", () => this.requestUpdate());
} }
render() { render() {
var _a, _b, _c; var _a, _b, _c, _d;
return $ ` return $ `
<ha-app-layout> <ha-app-layout>
<app-header slot="header" fixed> <app-header slot="header" fixed>
@ -137,7 +140,13 @@ loadDevTools().then(() => {
></ha-icon> ></ha-icon>
`} `}
</h1> </h1>
<div class="card-content">Browser-mod not connected.</div> <div class="card-content">
<p>Settings that apply to this browser.</p>
<p>
It is strongly recommended to refresh your browser window
after any change to those settings.
</p>
</div>
<div class="card-content"> <div class="card-content">
<ha-settings-row> <ha-settings-row>
<span slot="heading">Enable</span> <span slot="heading">Enable</span>
@ -168,7 +177,10 @@ loadDevTools().then(() => {
>Get camera input from this device (hardware >Get camera input from this device (hardware
dependent)</span dependent)</span
> >
<ha-switch> </ha-switch> <ha-switch
.checked=${(_d = window.browser_mod) === null || _d === void 0 ? void 0 : _d.cameraEnabled}
@change=${this.toggleCameraEnabled}
></ha-switch>
</ha-settings-row> </ha-settings-row>
</div> </div>
</ha-card> </ha-card>
@ -184,7 +196,10 @@ loadDevTools().then(() => {
.datetime=${window.browser_mod.devices[d].last_seen} .datetime=${window.browser_mod.devices[d].last_seen}
></ha-relative-time> ></ha-relative-time>
</span> </span>
<ha-icon-button .deviceID=${d} @click=${this.unregister_device}> <ha-icon-button
.deviceID=${d}
@click=${this.unregister_device}
>
<ha-icon .icon=${"mdi:delete"}></ha-icon> <ha-icon .icon=${"mdi:delete"}></ha-icon>
</ha-icon-button> </ha-icon-button>
<ha-icon-button> <ha-icon-button>

View File

@ -1,45 +1,40 @@
from datetime import datetime
import base64 import base64
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from .helpers import setup_platform, BrowserModEntity from .helpers import BrowserModEntity
from .const import DOMAIN, DATA_ADDERS
import logging import logging
PLATFORM = "camera"
LOGGER = logging.Logger(__name__) LOGGER = logging.Logger(__name__)
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async def async_setup_platform(
return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModCamera) 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): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModCamera(Camera, BrowserModEntity): class BrowserModCamera(BrowserModEntity, Camera):
domain = PLATFORM def __init__(self, coordinator, deviceID):
BrowserModEntity.__init__(self, coordinator, deviceID, None)
def __init__(self, hass, connection, deviceID, alias=None):
Camera.__init__(self) Camera.__init__(self)
BrowserModEntity.__init__(self, hass, connection, deviceID, alias)
self.last_seen = None
def updated(self):
if self.last_seen is None or (datetime.now() - self.last_seen).seconds > 15:
self.last_seen = datetime.now()
self.schedule_update_ha_state()
def camera_image(self, width=None, height=None):
return base64.b64decode(self.data.split(",")[-1])
@property @property
def extra_state_attributes(self): def unique_id(self):
return { return f"{self.deviceID}-camera"
"type": "browser_mod",
"deviceID": self.deviceID, @property
"last_seen": self.last_seen, def entity_registry_visible_default(self):
} return True
def camera_image(self, width=None, height=None):
if "camera" not in self._data:
LOGGER.error(self._data)
return None
return base64.b64decode(self._data["camera"].split(",")[-1])

View File

@ -15,7 +15,7 @@ from .const import WS_CONNECT, WS_REGISTER, WS_UNREGISTER, WS_REREGISTER, WS_UPD
from .helpers import get_devices, create_entity, get_config, is_setup_complete from .helpers import get_devices, create_entity, get_config, is_setup_complete
from .coordinator import Coordinator from .coordinator import Coordinator
from .device import getDevice from .device import getDevice, deleteDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,6 +40,7 @@ async def async_setup_connection(hass):
if store.get_device(deviceID).enabled: if store.get_device(deviceID).enabled:
dev = getDevice(hass, deviceID) dev = getDevice(hass, deviceID)
dev.update_settings(hass, store.get_device(deviceID).asdict())
dev.connection = (connection, msg["id"]) dev.connection = (connection, msg["id"])
await store.set_device(deviceID, await store.set_device(deviceID,
last_seen=datetime.now( last_seen=datetime.now(
@ -75,12 +76,8 @@ async def async_setup_connection(hass):
async def handle_unregister(hass, connection, msg): async def handle_unregister(hass, connection, msg):
deviceID = msg["deviceID"] deviceID = msg["deviceID"]
store = hass.data[DOMAIN]["store"] store = hass.data[DOMAIN]["store"]
devices = hass.data[DOMAIN]["devices"]
if deviceID in devices:
devices[deviceID].delete(hass)
del devices[deviceID]
deleteDevice(hass, deviceID)
await store.delete_device(deviceID) await store.delete_device(deviceID)
connection.send_result(msg["id"]) connection.send_result(msg["id"])
@ -96,28 +93,29 @@ async def async_setup_connection(hass):
async def handle_reregister(hass, connection, msg): async def handle_reregister(hass, connection, msg):
deviceID = msg["deviceID"] deviceID = msg["deviceID"]
store = hass.data[DOMAIN]["store"] store = hass.data[DOMAIN]["store"]
devices = hass.data[DOMAIN]["devices"]
data = msg["data"] data = msg["data"]
del data["last_seen"] del data["last_seen"]
device = {} deviceSettings = {}
if "deviceID" in data: if "deviceID" in data:
newDeviceID = data["deviceID"] newDeviceID = data["deviceID"]
del data["deviceID"] del data["deviceID"]
oldDevice = store.get_device(deviceID) oldDeviceSettings = store.get_device(deviceID)
if oldDevice: if oldDeviceSettings:
device = oldDevice.asdict() deviceSettings = oldDeviceSettings.asdict()
await store.delete_device(deviceID) await store.delete_device(deviceID)
if deviceID in devices: deleteDevice(hass, deviceID)
devices[deviceID].delete(hass)
del devices[deviceID]
deviceID = newDeviceID deviceID = newDeviceID
device.update(data) if (dev := getDevice(hass, deviceID, create=False)) is not None:
await store.set_device(deviceID, **device) dev.update_settings(hass, data)
deviceSettings.update(data)
await store.set_device(deviceID, **deviceSettings)
@websocket_api.websocket_command( @websocket_api.websocket_command(

View File

@ -9,29 +9,32 @@ from .sensor import BrowserSensor
from .light import BrowserModLight from .light import BrowserModLight
from .binary_sensor import BrowserBinarySensor from .binary_sensor import BrowserBinarySensor
from .media_player import BrowserModPlayer from .media_player import BrowserModPlayer
from .camera import BrowserModCamera
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BROWSER_SENSORS = {
"battery_level", ()
}
class BrowserModDevice: class BrowserModDevice:
""" A Browser_mod device. """ """A Browser_mod device."""
def __init__(self, hass, deviceID): def __init__(self, hass, deviceID):
""" """ """ """
self.deviceID = deviceID self.deviceID = deviceID
self.coordinator = Coordinator(hass, deviceID) self.coordinator = Coordinator(hass, deviceID)
self.entities = [] self.entities = []
self.camera_entity = None
self.data = {} self.data = {}
self.setup_sensors(hass) self.setup_sensors(hass)
self.connection = None self.connection = None
def update_settings(self, hass, settings):
if settings.get("camera", False) and self.camera_entity is None:
self.add_camera(hass)
elif self.camera_entity and not settings.get("camera", False):
self.remove_camera(hass)
def setup_sensors(self, hass): def setup_sensors(self, hass):
""" Create all entities associated with the device. """ """Create all entities associated with the device."""
coordinator = self.coordinator coordinator = self.coordinator
deviceID = self.deviceID deviceID = self.deviceID
@ -70,9 +73,30 @@ class BrowserModDevice:
adder(new) adder(new)
self.entities += new self.entities += new
def add_camera(self, hass):
if self.camera_entity is not None:
return
coordinator = self.coordinator
deviceID = self.deviceID
adder = hass.data[DOMAIN][DATA_ADDERS]["camera"]
self.camera_entity = BrowserModCamera(coordinator, deviceID)
adder([self.camera_entity])
self.entities.append(self.camera_entity)
pass
def remove_camera(self, hass):
if self.camera_entity is None:
return
er = entity_registry.async_get(hass)
er.async_remove(self.camera_entity.entity_id)
self.entities.remove(self.camera_entity)
self.camera_entity = None
pass
def send(self, command, **kwargs): def send(self, command, **kwargs):
""" Send a command to this device. """ """Send a command to this device."""
if self.connection is None: return if self.connection is None:
return
connection, cid = self.connection connection, cid = self.connection
@ -87,22 +111,35 @@ class BrowserModDevice:
) )
def delete(self, hass): def delete(self, hass):
""" Delete device and associated entities. """ """Delete device and associated entities."""
dr = device_registry.async_get(hass) dr = device_registry.async_get(hass)
er = entity_registry.async_get(hass) er = entity_registry.async_get(hass)
for e in self.entities: for e in self.entities:
er.async_remove(e.entity_id) er.async_remove(e.entity_id)
self.entities = []
self.camera_entity = None
device = dr.async_get_device({(DOMAIN, self.deviceID)}) device = dr.async_get_device({(DOMAIN, self.deviceID)})
dr.async_remove_device(device.id) dr.async_remove_device(device.id)
def getDevice(hass, deviceID): def getDevice(hass, deviceID, *, create=True):
""" Get or create device by deviceID. """ """Get or create device by deviceID."""
devices = hass.data[DOMAIN]["devices"] devices = hass.data[DOMAIN]["devices"]
if deviceID in devices: if deviceID in devices:
return devices[deviceID] return devices[deviceID]
if not create:
return None
devices[deviceID] = BrowserModDevice(hass, deviceID) devices[deviceID] = BrowserModDevice(hass, deviceID)
return devices[deviceID] return devices[deviceID]
def deleteDevice(hass, deviceID):
devices = hass.data[DOMAIN]["devices"]
if deviceID in devices:
devices["deviceID"].delete()
del devices["deviceID"]

View File

@ -72,8 +72,8 @@ def setup_platform(hass, config, async_add_devices, platform, cls):
def is_setup_complete(hass): def is_setup_complete(hass):
return hass.data[DOMAIN][DATA_SETUP_COMPLETE] return hass.data[DOMAIN][DATA_SETUP_COMPLETE]
class BrowserModEntity2(CoordinatorEntity):
class BrowserModEntity(CoordinatorEntity):
def __init__(self, coordinator, deviceID, name): def __init__(self, coordinator, deviceID, name):
super().__init__(coordinator) super().__init__(coordinator)
self.deviceID = deviceID self.deviceID = deviceID
@ -101,55 +101,15 @@ class BrowserModEntity2(CoordinatorEntity):
@property @property
def name(self): def name(self):
return self._name return self._name
@property @property
def has_entity_name(self): def has_entity_name(self):
return True return True
@property @property
def entity_registry_visible_default(self): def entity_registry_visible_default(self):
return False return False
@property @property
def unique_id(self): def unique_id(self):
return f"{self.deviceID}-{self._name.replace(' ','_')}" return f"{self.deviceID}-{self._name.replace(' ','_')}"
class BrowserModEntity(Entity):
def __init__(self, hass, connection, deviceID, alias=None):
self.hass = hass
self.connection = connection
self.deviceID = deviceID
self._data = {}
self._alias = alias
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 device_info(self):
return {
"identifiers": {(DOMAIN, self.deviceID)},
"name": self._alias or self.deviceID,
}
@property
def unique_id(self):
return f"{self.domain}-{self.deviceID}"
@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)

View File

@ -1,20 +1,23 @@
from homeassistant.components.light import LightEntity, ColorMode from homeassistant.components.light import LightEntity, ColorMode
from .helpers import setup_platform, BrowserModEntity2 from .helpers import setup_platform, BrowserModEntity
from .const import DOMAIN, DATA_ADDERS from .const import DOMAIN, DATA_ADDERS
async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): async def async_setup_platform(
hass, config_entry, async_add_entities, discoveryInfo=None
):
hass.data[DOMAIN][DATA_ADDERS]["light"] = async_add_entities hass.data[DOMAIN][DATA_ADDERS]["light"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModLight(BrowserModEntity2, LightEntity): class BrowserModLight(BrowserModEntity, LightEntity):
def __init__(self, coordinator, deviceID, device): def __init__(self, coordinator, deviceID, device):
super().__init__(coordinator, deviceID, "Screen") BrowserModEntity.__init__(self, coordinator, deviceID, "Screen")
LightEntity.__init__(self)
self.device = device self.device = device
@property @property
@ -28,6 +31,7 @@ class BrowserModLight(BrowserModEntity2, LightEntity):
@property @property
def supported_color_modes(self): def supported_color_modes(self):
return {ColorMode.BRIGHTNESS} return {ColorMode.BRIGHTNESS}
@property @property
def color_mode(self): def color_mode(self):
return ColorMode.BRIGHTNESS return ColorMode.BRIGHTNESS

View File

@ -25,27 +25,34 @@ from homeassistant.const import (
STATE_UNKNOWN, STATE_UNKNOWN,
) )
from .helpers import BrowserModEntity2 from .helpers import BrowserModEntity
from .const import DOMAIN, DATA_ADDERS from .const import DOMAIN, DATA_ADDERS
async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): async def async_setup_platform(
hass, config_entry, async_add_entities, discoveryInfo=None
):
hass.data[DOMAIN][DATA_ADDERS]["media_player"] = async_add_entities hass.data[DOMAIN][DATA_ADDERS]["media_player"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity): class BrowserModPlayer(BrowserModEntity, MediaPlayerEntity):
def __init__(self, coordinator, deviceID, device): def __init__(self, coordinator, deviceID, device):
super().__init__(coordinator, deviceID, None) BrowserModEntity.__init__(self, coordinator, deviceID, None)
MediaPlayerEntity.__init__(self)
self.device = device self.device = device
@property @property
def unique_id(self): def unique_id(self):
return f"{self.deviceID}-player" return f"{self.deviceID}-player"
@property
def entity_registry_visible_default(self):
return True
@property @property
def state(self): def state(self):
state = self._data.get("player", {}).get("state") state = self._data.get("player", {}).get("state")
@ -76,7 +83,6 @@ class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity):
def is_volume_muted(self): def is_volume_muted(self):
return self._data.get("player", {}).get("muted", False) return self._data.get("player", {}).get("muted", False)
def set_volume_level(self, volume): def set_volume_level(self, volume):
self.device.send("player-set-volume", volume_level=volume) self.device.send("player-set-volume", volume_level=volume)
@ -86,7 +92,9 @@ class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity):
async def async_play_media(self, media_type, media_id, **kwargs): async def async_play_media(self, media_type, media_id, **kwargs):
if media_source.is_media_source_id(media_id): if media_source.is_media_source_id(media_id):
media_type = MEDIA_TYPE_URL media_type = MEDIA_TYPE_URL
play_item = await media_source.async_resolve_media(self.hass, media_id, self.entity_id) play_item = await media_source.async_resolve_media(
self.hass, media_id, self.entity_id
)
media_id = play_item.url media_id = play_item.url
if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC):
media_id = async_process_play_media_url(self.hass, media_id) media_id = async_process_play_media_url(self.hass, media_id)

View File

@ -1,22 +1,29 @@
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from .const import DOMAIN, DATA_ADDERS from .const import DOMAIN, DATA_ADDERS
from .helpers import BrowserModEntity2 from .helpers import BrowserModEntity
async def async_setup_platform(hass, config_entry, async_add_entities, discoveryInfo = None): async def async_setup_platform(
hass, config_entry, async_add_entities, discoveryInfo=None
):
hass.data[DOMAIN][DATA_ADDERS]["sensor"] = async_add_entities hass.data[DOMAIN][DATA_ADDERS]["sensor"] = async_add_entities
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_platform(hass, {}, async_add_entities) await async_setup_platform(hass, {}, async_add_entities)
class BrowserSensor(BrowserModEntity2, SensorEntity): class BrowserSensor(BrowserModEntity, SensorEntity):
def __init__(self, coordinator, deviceID, parameter, def __init__(
name, self,
unit_of_measurement = None, coordinator,
device_class = None, deviceID,
): parameter,
name,
unit_of_measurement=None,
device_class=None,
):
super().__init__(coordinator, deviceID, name) super().__init__(coordinator, deviceID, name)
self.parameter = parameter self.parameter = parameter
self._device_class = device_class self._device_class = device_class
@ -28,9 +35,11 @@ class BrowserSensor(BrowserModEntity2, SensorEntity):
data = data.get("browser", {}) data = data.get("browser", {})
data = data.get(self.parameter, None) data = data.get(self.parameter, None)
return data return data
@property @property
def device_class(self): def device_class(self):
return self._device_class return self._device_class
@property @property
def native_unit_of_measurement(self): def native_unit_of_measurement(self):
return self._unit_of_measurement return self._unit_of_measurement
@ -41,8 +50,10 @@ class BrowserSensor(BrowserModEntity2, SensorEntity):
if self.parameter == "currentUser": if self.parameter == "currentUser":
retval["userData"] = self._data.get("browser", {}).get("userData") retval["userData"] = self._data.get("browser", {}).get("userData")
if self.parameter == "path": if self.parameter == "path":
retval["pathSegments"] = self._data.get("browser", {}).get("path", "").split("/") retval["pathSegments"] = (
self._data.get("browser", {}).get("path", "").split("/")
)
if self.parameter == "userAgent": if self.parameter == "userAgent":
retval["userAgent"] = self._data.get("browser", {}).get("userAgent") retval["userAgent"] = self._data.get("browser", {}).get("userAgent")
return retval return retval

View File

@ -26,22 +26,28 @@ class DeviceStoreData:
def asdict(self): def asdict(self):
return attr.asdict(self) return attr.asdict(self)
@attr.s @attr.s
class ConfigStoreData: class ConfigStoreData:
devices = attr.ib(type=dict[str: DeviceStoreData], factory=dict) devices = attr.ib(type=dict[str:DeviceStoreData], factory=dict)
version = attr.ib(type=str, default="2.0") version = attr.ib(type=str, default="2.0")
@classmethod @classmethod
def from_dict(cls, data): def from_dict(cls, data={}):
devices = {k: DeviceStoreData.from_dict(v) for k,v in data["devices"].items()} devices = {k: DeviceStoreData.from_dict(v) for k, v in data["devices"].items()}
return cls(**(data | { return cls(
"devices": devices, **(
} data
)) | {
"devices": devices,
}
)
)
def asdict(self): def asdict(self):
return attr.asdict(self) return attr.asdict(self)
class BrowserModStore: class BrowserModStore:
def __init__(self, hass): def __init__(self, hass):
self.store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self.store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
@ -55,10 +61,12 @@ class BrowserModStore:
self.dirty = False self.dirty = False
async def load(self): async def load(self):
self.data = ConfigStoreData.from_dict(await self.store.async_load()) stored = await self.store.async_load()
if stored:
self.data = ConfigStoreData.from_dict(stored)
if self.data is None: if self.data is None:
self.data = ConfigStoreData() self.data = ConfigStoreData()
self.save() await self.save()
self.dirty = False self.dirty = False
async def updated(self): async def updated(self):

View File

@ -17,6 +17,9 @@ loadDevTools().then(() => {
changeDeviceID(ev) { changeDeviceID(ev) {
window.browser_mod.deviceID = ev.target.value; window.browser_mod.deviceID = ev.target.value;
} }
toggleCameraEnabled() {
window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled;
}
unregister_device(ev) { unregister_device(ev) {
const deviceID = ev.currentTarget.deviceID; const deviceID = ev.currentTarget.deviceID;
@ -68,7 +71,13 @@ loadDevTools().then(() => {
></ha-icon> ></ha-icon>
`} `}
</h1> </h1>
<div class="card-content">Browser-mod not connected.</div> <div class="card-content">
<p>Settings that apply to this browser.</p>
<p>
It is strongly recommended to refresh your browser window
after any change to those settings.
</p>
</div>
<div class="card-content"> <div class="card-content">
<ha-settings-row> <ha-settings-row>
<span slot="heading">Enable</span> <span slot="heading">Enable</span>
@ -99,7 +108,10 @@ loadDevTools().then(() => {
>Get camera input from this device (hardware >Get camera input from this device (hardware
dependent)</span dependent)</span
> >
<ha-switch> </ha-switch> <ha-switch
.checked=${window.browser_mod?.cameraEnabled}
@change=${this.toggleCameraEnabled}
></ha-switch>
</ha-settings-row> </ha-settings-row>
</div> </div>
</ha-card> </ha-card>
@ -116,7 +128,10 @@ loadDevTools().then(() => {
.datetime=${window.browser_mod.devices[d].last_seen} .datetime=${window.browser_mod.devices[d].last_seen}
></ha-relative-time> ></ha-relative-time>
</span> </span>
<ha-icon-button .deviceID=${d} @click=${this.unregister_device}> <ha-icon-button
.deviceID=${d}
@click=${this.unregister_device}
>
<ha-icon .icon=${"mdi:delete"}></ha-icon> <ha-icon .icon=${"mdi:delete"}></ha-icon>
</ha-icon-button> </ha-icon-button>
<ha-icon-button> <ha-icon-button>

View File

@ -1,63 +1,72 @@
export const BrowserModCameraMixin = (C) => export const CameraMixin = (SuperClass) => {
class extends C { return class CameraMixinClass extends SuperClass {
setup_camera() { private _video;
console.log("Starting camera"); private _canvas;
private _framerate;
if (this._video) return; constructor() {
this._video = document.createElement("video"); super();
this._video.autoplay = true; this._framerate = 2;
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( window.addEventListener(
"click", "pointerdown",
() => { () => {
if (this._video.ended || this._video.paused) this._video.play(); this._setup_camera();
}, },
{ { once: true }
once: true,
}
); );
} }
update_camera() { async _setup_camera() {
this._canvas.width = this._video.videoWidth; if (this._video) return;
this._canvas.height = this._video.videoHeight; await this.connectionPromise;
if (!this.cameraEnabled) return;
const video = (this._video = document.createElement("video"));
video.autoplay = true;
video.playsInline = true;
video.style.display = "none";
const canvas = (this._canvas = document.createElement("canvas"));
canvas.style.display = "none";
document.body.appendChild(video);
document.body.appendChild(canvas);
if (!navigator.mediaDevices) return;
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
video.srcObject = stream;
video.play();
this.update_camera();
}
async update_camera() {
if (!this.cameraEnabled) {
const stream = this._video?.srcObject;
if (stream) {
stream.getTracks().forEach((t) => t.stop());
this._video.scrObject = undefined;
}
return;
}
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"); const context = this._canvas.getContext("2d");
context.drawImage( context.drawImage(video, 0, 0, width, height);
this._video,
0,
0,
this._video.videoWidth,
this._video.videoHeight
);
this.sendUpdate({ this.sendUpdate({
camera: this._canvas.toDataURL("image/jpeg"), camera: this._canvas.toDataURL("image/jpeg"),
}); });
setTimeout(
() => this.update_camera(), const interval = Math.round(1000 / this._framerate);
Math.round(1000 / this._camera_framerate) setTimeout(() => this.update_camera(), interval);
);
} }
}; };
};

View File

@ -103,21 +103,32 @@ export const ConnectionMixin = (SuperClass) => {
})(); })();
} }
private async _reregister(newData = {}) {
await this.connection.sendMessage({
type: "browser_mod/reregister",
deviceID: this.deviceID,
data: {
...this.devices[this.deviceID],
...newData,
},
});
}
get meta() { get meta() {
if (!this.registered) return null; if (!this.registered) return null;
return this.devices[this.deviceID].meta; return this.devices[this.deviceID].meta;
} }
set meta(value) { set meta(value) {
(async () => { this._reregister({ meta: value });
await this.connection.sendMessage({ }
type: "browser_mod/reregister",
deviceID: this.deviceID, get cameraEnabled() {
data: { if (!this.registered) return null;
...this.devices[this.deviceID], return this.devices[this.deviceID].camera;
meta: value, }
} set cameraEnabled(value) {
}) this._reregister({ camera: value });
})()
} }
sendUpdate(data) { sendUpdate(data) {

View File

@ -9,8 +9,8 @@ import "./browser-player";
import { ConnectionMixin } from "./connection"; import { ConnectionMixin } from "./connection";
import { ScreenSaverMixin } from "./screensaver"; import { ScreenSaverMixin } from "./screensaver";
import { MediaPlayerMixin } from "./mediaPlayer"; import { MediaPlayerMixin } from "./mediaPlayer";
import { CameraMixin } from "./camera";
import { FullyKioskMixin } from "./fullyKiosk"; import { FullyKioskMixin } from "./fullyKiosk";
import { BrowserModCameraMixin } from "./camera";
import { BrowserModScreensaverMixin } from "./screensaver"; import { BrowserModScreensaverMixin } from "./screensaver";
import { BrowserModPopupsMixin } from "./popups"; import { BrowserModPopupsMixin } from "./popups";
import { BrowserModBrowserMixin } from "./browser"; import { BrowserModBrowserMixin } from "./browser";
@ -27,7 +27,9 @@ const ext = (baseClass, mixins) =>
// FullyKioskMixin, // FullyKioskMixin,
// BrowserModMediaPlayerMixin, // BrowserModMediaPlayerMixin,
// ]) { // ]) {
export class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) { export class BrowserMod extends CameraMixin(
MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget)))
) {
constructor() { constructor() {
super(); super();
this.entity_id = deviceID.replace("-", "_"); this.entity_id = deviceID.replace("-", "_");

2
package-lock.json generated
View File

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

View File

@ -25,4 +25,4 @@
"dependencies": { "dependencies": {
"card-tools": "github:thomasloven/lovelace-card-tools" "card-tools": "github:thomasloven/lovelace-card-tools"
} }
} }