Added camera functionality
This commit is contained in:
parent
466a5eb5e7
commit
e4a65f3077
@ -3,14 +3,7 @@ import logging
|
||||
from .store import BrowserModStore
|
||||
from .mod_view import async_setup_view
|
||||
from .connection import async_setup_connection
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
DATA_DEVICES,
|
||||
DATA_ADDERS,
|
||||
DATA_STORE
|
||||
)
|
||||
|
||||
from .coordinator import Coordinator
|
||||
from .const import DOMAIN, DATA_DEVICES, DATA_ADDERS, DATA_STORE
|
||||
|
||||
_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, "light")
|
||||
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_view(hass)
|
||||
|
||||
return True
|
||||
for component in COMPONENTS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
)
|
||||
return True
|
||||
|
@ -1,18 +1,20 @@
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, 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):
|
||||
super().__init__(coordinator, deviceID, name)
|
||||
self.parameter = parameter
|
||||
|
@ -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() {
|
||||
if (!this.registered)
|
||||
return null;
|
||||
return this.devices[this.deviceID].meta;
|
||||
}
|
||||
set meta(value) {
|
||||
(async () => {
|
||||
await this.connection.sendMessage({
|
||||
type: "browser_mod/reregister",
|
||||
deviceID: this.deviceID,
|
||||
data: Object.assign(Object.assign({}, this.devices[this.deviceID]), { meta: value })
|
||||
});
|
||||
})();
|
||||
this._reregister({ meta: value });
|
||||
}
|
||||
get cameraEnabled() {
|
||||
if (!this.registered)
|
||||
return null;
|
||||
return this.devices[this.deviceID].camera;
|
||||
}
|
||||
set cameraEnabled(value) {
|
||||
this._reregister({ camera: value });
|
||||
}
|
||||
sendUpdate(data) {
|
||||
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 version = "2.0.0b0";
|
||||
var description = "";
|
||||
@ -970,7 +1038,7 @@ var pjson = {
|
||||
// FullyKioskMixin,
|
||||
// BrowserModMediaPlayerMixin,
|
||||
// ]) {
|
||||
class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) {
|
||||
class BrowserMod extends CameraMixin(MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget)))) {
|
||||
constructor() {
|
||||
super();
|
||||
this.entity_id = deviceID.replace("-", "_");
|
||||
|
@ -90,6 +90,9 @@ loadDevTools().then(() => {
|
||||
changeDeviceID(ev) {
|
||||
window.browser_mod.deviceID = ev.target.value;
|
||||
}
|
||||
toggleCameraEnabled() {
|
||||
window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled;
|
||||
}
|
||||
unregister_device(ev) {
|
||||
const deviceID = ev.currentTarget.deviceID;
|
||||
if (deviceID === window.browser_mod.deviceID)
|
||||
@ -104,7 +107,7 @@ loadDevTools().then(() => {
|
||||
window.browser_mod.addEventListener("browser-mod-config-update", () => this.requestUpdate());
|
||||
}
|
||||
render() {
|
||||
var _a, _b, _c;
|
||||
var _a, _b, _c, _d;
|
||||
return $ `
|
||||
<ha-app-layout>
|
||||
<app-header slot="header" fixed>
|
||||
@ -137,7 +140,13 @@ loadDevTools().then(() => {
|
||||
></ha-icon>
|
||||
`}
|
||||
</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">
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Enable</span>
|
||||
@ -168,7 +177,10 @@ loadDevTools().then(() => {
|
||||
>Get camera input from this device (hardware
|
||||
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>
|
||||
</div>
|
||||
</ha-card>
|
||||
@ -184,7 +196,10 @@ loadDevTools().then(() => {
|
||||
.datetime=${window.browser_mod.devices[d].last_seen}
|
||||
></ha-relative-time>
|
||||
</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-button>
|
||||
<ha-icon-button>
|
||||
|
@ -1,45 +1,40 @@
|
||||
from datetime import datetime
|
||||
import base64
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
|
||||
from .helpers import setup_platform, BrowserModEntity
|
||||
from .helpers import 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)
|
||||
async def async_setup_platform(
|
||||
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(Camera, BrowserModEntity):
|
||||
domain = PLATFORM
|
||||
|
||||
def __init__(self, hass, connection, deviceID, alias=None):
|
||||
class BrowserModCamera(BrowserModEntity, Camera):
|
||||
def __init__(self, coordinator, deviceID):
|
||||
BrowserModEntity.__init__(self, coordinator, deviceID, None)
|
||||
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
|
||||
def extra_state_attributes(self):
|
||||
return {
|
||||
"type": "browser_mod",
|
||||
"deviceID": self.deviceID,
|
||||
"last_seen": self.last_seen,
|
||||
}
|
||||
def unique_id(self):
|
||||
return f"{self.deviceID}-camera"
|
||||
|
||||
@property
|
||||
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])
|
||||
|
@ -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 .coordinator import Coordinator
|
||||
from .device import getDevice
|
||||
from .device import getDevice, deleteDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -40,6 +40,7 @@ async def async_setup_connection(hass):
|
||||
|
||||
if store.get_device(deviceID).enabled:
|
||||
dev = getDevice(hass, deviceID)
|
||||
dev.update_settings(hass, store.get_device(deviceID).asdict())
|
||||
dev.connection = (connection, msg["id"])
|
||||
await store.set_device(deviceID,
|
||||
last_seen=datetime.now(
|
||||
@ -75,12 +76,8 @@ async def async_setup_connection(hass):
|
||||
async def handle_unregister(hass, connection, msg):
|
||||
deviceID = msg["deviceID"]
|
||||
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)
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
@ -96,28 +93,29 @@ async def async_setup_connection(hass):
|
||||
async def handle_reregister(hass, connection, msg):
|
||||
deviceID = msg["deviceID"]
|
||||
store = hass.data[DOMAIN]["store"]
|
||||
devices = hass.data[DOMAIN]["devices"]
|
||||
|
||||
data = msg["data"]
|
||||
del data["last_seen"]
|
||||
device = {}
|
||||
deviceSettings = {}
|
||||
|
||||
if "deviceID" in data:
|
||||
newDeviceID = data["deviceID"]
|
||||
del data["deviceID"]
|
||||
|
||||
oldDevice = store.get_device(deviceID)
|
||||
if oldDevice:
|
||||
device = oldDevice.asdict()
|
||||
oldDeviceSettings = store.get_device(deviceID)
|
||||
if oldDeviceSettings:
|
||||
deviceSettings = oldDeviceSettings.asdict()
|
||||
await store.delete_device(deviceID)
|
||||
|
||||
if deviceID in devices:
|
||||
devices[deviceID].delete(hass)
|
||||
del devices[deviceID]
|
||||
deleteDevice(hass, deviceID)
|
||||
|
||||
deviceID = newDeviceID
|
||||
|
||||
device.update(data)
|
||||
await store.set_device(deviceID, **device)
|
||||
if (dev := getDevice(hass, deviceID, create=False)) is not None:
|
||||
dev.update_settings(hass, data)
|
||||
|
||||
deviceSettings.update(data)
|
||||
await store.set_device(deviceID, **deviceSettings)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
|
@ -9,14 +9,10 @@ from .sensor import BrowserSensor
|
||||
from .light import BrowserModLight
|
||||
from .binary_sensor import BrowserBinarySensor
|
||||
from .media_player import BrowserModPlayer
|
||||
from .camera import BrowserModCamera
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BROWSER_SENSORS = {
|
||||
"battery_level", ()
|
||||
}
|
||||
|
||||
|
||||
|
||||
class BrowserModDevice:
|
||||
"""A Browser_mod device."""
|
||||
@ -26,10 +22,17 @@ class BrowserModDevice:
|
||||
self.deviceID = deviceID
|
||||
self.coordinator = Coordinator(hass, deviceID)
|
||||
self.entities = []
|
||||
self.camera_entity = None
|
||||
self.data = {}
|
||||
self.setup_sensors(hass)
|
||||
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):
|
||||
"""Create all entities associated with the device."""
|
||||
|
||||
@ -70,9 +73,30 @@ class BrowserModDevice:
|
||||
adder(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):
|
||||
"""Send a command to this device."""
|
||||
if self.connection is None: return
|
||||
if self.connection is None:
|
||||
return
|
||||
|
||||
connection, cid = self.connection
|
||||
|
||||
@ -94,15 +118,28 @@ class BrowserModDevice:
|
||||
for e in self.entities:
|
||||
er.async_remove(e.entity_id)
|
||||
|
||||
self.entities = []
|
||||
self.camera_entity = None
|
||||
|
||||
device = dr.async_get_device({(DOMAIN, self.deviceID)})
|
||||
dr.async_remove_device(device.id)
|
||||
|
||||
|
||||
def getDevice(hass, deviceID):
|
||||
def getDevice(hass, deviceID, *, create=True):
|
||||
"""Get or create device by deviceID."""
|
||||
devices = hass.data[DOMAIN]["devices"]
|
||||
if deviceID in devices:
|
||||
return devices[deviceID]
|
||||
|
||||
if not create:
|
||||
return None
|
||||
|
||||
devices[deviceID] = BrowserModDevice(hass, deviceID)
|
||||
return devices[deviceID]
|
||||
|
||||
|
||||
def deleteDevice(hass, deviceID):
|
||||
devices = hass.data[DOMAIN]["devices"]
|
||||
if deviceID in devices:
|
||||
devices["deviceID"].delete()
|
||||
del devices["deviceID"]
|
||||
|
@ -72,8 +72,8 @@ def setup_platform(hass, config, async_add_devices, platform, cls):
|
||||
def is_setup_complete(hass):
|
||||
return hass.data[DOMAIN][DATA_SETUP_COMPLETE]
|
||||
|
||||
class BrowserModEntity2(CoordinatorEntity):
|
||||
|
||||
class BrowserModEntity(CoordinatorEntity):
|
||||
def __init__(self, coordinator, deviceID, name):
|
||||
super().__init__(coordinator)
|
||||
self.deviceID = deviceID
|
||||
@ -101,55 +101,15 @@ class BrowserModEntity2(CoordinatorEntity):
|
||||
@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.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)
|
||||
|
@ -1,20 +1,23 @@
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, 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):
|
||||
super().__init__(coordinator, deviceID, "Screen")
|
||||
BrowserModEntity.__init__(self, coordinator, deviceID, "Screen")
|
||||
LightEntity.__init__(self)
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
@ -28,6 +31,7 @@ class BrowserModLight(BrowserModEntity2, LightEntity):
|
||||
@property
|
||||
def supported_color_modes(self):
|
||||
return {ColorMode.BRIGHTNESS}
|
||||
|
||||
@property
|
||||
def color_mode(self):
|
||||
return ColorMode.BRIGHTNESS
|
||||
|
@ -25,27 +25,34 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from .helpers import BrowserModEntity2
|
||||
from .helpers import BrowserModEntity
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, 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):
|
||||
super().__init__(coordinator, deviceID, None)
|
||||
BrowserModEntity.__init__(self, coordinator, deviceID, None)
|
||||
MediaPlayerEntity.__init__(self)
|
||||
self.device = device
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return f"{self.deviceID}-player"
|
||||
|
||||
@property
|
||||
def entity_registry_visible_default(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
state = self._data.get("player", {}).get("state")
|
||||
@ -76,7 +83,6 @@ class BrowserModPlayer(BrowserModEntity2, MediaPlayerEntity):
|
||||
def is_volume_muted(self):
|
||||
return self._data.get("player", {}).get("muted", False)
|
||||
|
||||
|
||||
def set_volume_level(self, 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):
|
||||
if media_source.is_media_source_id(media_id):
|
||||
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
|
||||
if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC):
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
|
@ -1,18 +1,25 @@
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
await async_setup_platform(hass, {}, async_add_entities)
|
||||
|
||||
|
||||
class BrowserSensor(BrowserModEntity2, SensorEntity):
|
||||
def __init__(self, coordinator, deviceID, parameter,
|
||||
class BrowserSensor(BrowserModEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator,
|
||||
deviceID,
|
||||
parameter,
|
||||
name,
|
||||
unit_of_measurement=None,
|
||||
device_class=None,
|
||||
@ -28,9 +35,11 @@ class BrowserSensor(BrowserModEntity2, SensorEntity):
|
||||
data = data.get("browser", {})
|
||||
data = data.get(self.parameter, None)
|
||||
return data
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
return self._unit_of_measurement
|
||||
@ -41,7 +50,9 @@ class BrowserSensor(BrowserModEntity2, SensorEntity):
|
||||
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("/")
|
||||
retval["pathSegments"] = (
|
||||
self._data.get("browser", {}).get("path", "").split("/")
|
||||
)
|
||||
if self.parameter == "userAgent":
|
||||
retval["userAgent"] = self._data.get("browser", {}).get("userAgent")
|
||||
|
||||
|
@ -26,22 +26,28 @@ class DeviceStoreData:
|
||||
def asdict(self):
|
||||
return attr.asdict(self)
|
||||
|
||||
|
||||
@attr.s
|
||||
class ConfigStoreData:
|
||||
devices = attr.ib(type=dict[str:DeviceStoreData], factory=dict)
|
||||
version = attr.ib(type=str, default="2.0")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
def from_dict(cls, data={}):
|
||||
devices = {k: DeviceStoreData.from_dict(v) for k, v in data["devices"].items()}
|
||||
return cls(**(data | {
|
||||
return cls(
|
||||
**(
|
||||
data
|
||||
| {
|
||||
"devices": devices,
|
||||
}
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
def asdict(self):
|
||||
return attr.asdict(self)
|
||||
|
||||
|
||||
class BrowserModStore:
|
||||
def __init__(self, hass):
|
||||
self.store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
@ -55,10 +61,12 @@ class BrowserModStore:
|
||||
self.dirty = False
|
||||
|
||||
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:
|
||||
self.data = ConfigStoreData()
|
||||
self.save()
|
||||
await self.save()
|
||||
self.dirty = False
|
||||
|
||||
async def updated(self):
|
||||
|
@ -17,6 +17,9 @@ loadDevTools().then(() => {
|
||||
changeDeviceID(ev) {
|
||||
window.browser_mod.deviceID = ev.target.value;
|
||||
}
|
||||
toggleCameraEnabled() {
|
||||
window.browser_mod.cameraEnabled = !window.browser_mod.cameraEnabled;
|
||||
}
|
||||
|
||||
unregister_device(ev) {
|
||||
const deviceID = ev.currentTarget.deviceID;
|
||||
@ -68,7 +71,13 @@ loadDevTools().then(() => {
|
||||
></ha-icon>
|
||||
`}
|
||||
</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">
|
||||
<ha-settings-row>
|
||||
<span slot="heading">Enable</span>
|
||||
@ -99,7 +108,10 @@ loadDevTools().then(() => {
|
||||
>Get camera input from this device (hardware
|
||||
dependent)</span
|
||||
>
|
||||
<ha-switch> </ha-switch>
|
||||
<ha-switch
|
||||
.checked=${window.browser_mod?.cameraEnabled}
|
||||
@change=${this.toggleCameraEnabled}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>
|
||||
@ -116,7 +128,10 @@ loadDevTools().then(() => {
|
||||
.datetime=${window.browser_mod.devices[d].last_seen}
|
||||
></ha-relative-time>
|
||||
</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-button>
|
||||
<ha-icon-button>
|
||||
|
@ -1,63 +1,72 @@
|
||||
export const BrowserModCameraMixin = (C) =>
|
||||
class extends C {
|
||||
setup_camera() {
|
||||
console.log("Starting camera");
|
||||
export const CameraMixin = (SuperClass) => {
|
||||
return class CameraMixinClass extends SuperClass {
|
||||
private _video;
|
||||
private _canvas;
|
||||
private _framerate;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._framerate = 2;
|
||||
|
||||
window.addEventListener(
|
||||
"pointerdown",
|
||||
() => {
|
||||
this._setup_camera();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
async _setup_camera() {
|
||||
if (this._video) return;
|
||||
this._video = document.createElement("video");
|
||||
this._video.autoplay = true;
|
||||
this._video.playsInline = true;
|
||||
this._video.style.display = "none";
|
||||
await this.connectionPromise;
|
||||
if (!this.cameraEnabled) return;
|
||||
const video = (this._video = document.createElement("video"));
|
||||
video.autoplay = true;
|
||||
video.playsInline = true;
|
||||
video.style.display = "none";
|
||||
|
||||
this._canvas = document.createElement("canvas");
|
||||
this._canvas.style.display = "none";
|
||||
const canvas = (this._canvas = document.createElement("canvas"));
|
||||
canvas.style.display = "none";
|
||||
|
||||
document.body.appendChild(this._video);
|
||||
document.body.appendChild(this._canvas);
|
||||
document.body.appendChild(video);
|
||||
document.body.appendChild(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();
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: true,
|
||||
audio: false,
|
||||
});
|
||||
|
||||
this._camera_framerate = 2;
|
||||
|
||||
window.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
if (this._video.ended || this._video.paused) this._video.play();
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
this.update_camera();
|
||||
}
|
||||
|
||||
update_camera() {
|
||||
this._canvas.width = this._video.videoWidth;
|
||||
this._canvas.height = this._video.videoHeight;
|
||||
|
||||
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");
|
||||
context.drawImage(
|
||||
this._video,
|
||||
0,
|
||||
0,
|
||||
this._video.videoWidth,
|
||||
this._video.videoHeight
|
||||
);
|
||||
context.drawImage(video, 0, 0, width, height);
|
||||
|
||||
this.sendUpdate({
|
||||
camera: this._canvas.toDataURL("image/jpeg"),
|
||||
});
|
||||
setTimeout(
|
||||
() => this.update_camera(),
|
||||
Math.round(1000 / this._camera_framerate)
|
||||
);
|
||||
|
||||
const interval = Math.round(1000 / this._framerate);
|
||||
setTimeout(() => this.update_camera(), interval);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -103,21 +103,32 @@ export const ConnectionMixin = (SuperClass) => {
|
||||
})();
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (!this.registered) return null;
|
||||
return this.devices[this.deviceID].meta;
|
||||
}
|
||||
set meta(value) {
|
||||
(async () => {
|
||||
|
||||
private async _reregister(newData = {}) {
|
||||
await this.connection.sendMessage({
|
||||
type: "browser_mod/reregister",
|
||||
deviceID: this.deviceID,
|
||||
data: {
|
||||
...this.devices[this.deviceID],
|
||||
meta: value,
|
||||
...newData,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
||||
get meta() {
|
||||
if (!this.registered) return null;
|
||||
return this.devices[this.deviceID].meta;
|
||||
}
|
||||
set meta(value) {
|
||||
this._reregister({ meta: value });
|
||||
}
|
||||
|
||||
get cameraEnabled() {
|
||||
if (!this.registered) return null;
|
||||
return this.devices[this.deviceID].camera;
|
||||
}
|
||||
set cameraEnabled(value) {
|
||||
this._reregister({ camera: value });
|
||||
}
|
||||
|
||||
sendUpdate(data) {
|
||||
|
@ -9,8 +9,8 @@ import "./browser-player";
|
||||
import { ConnectionMixin } from "./connection";
|
||||
import { ScreenSaverMixin } from "./screensaver";
|
||||
import { MediaPlayerMixin } from "./mediaPlayer";
|
||||
import { CameraMixin } from "./camera";
|
||||
import { FullyKioskMixin } from "./fullyKiosk";
|
||||
import { BrowserModCameraMixin } from "./camera";
|
||||
import { BrowserModScreensaverMixin } from "./screensaver";
|
||||
import { BrowserModPopupsMixin } from "./popups";
|
||||
import { BrowserModBrowserMixin } from "./browser";
|
||||
@ -27,7 +27,9 @@ const ext = (baseClass, mixins) =>
|
||||
// FullyKioskMixin,
|
||||
// BrowserModMediaPlayerMixin,
|
||||
// ]) {
|
||||
export class BrowserMod extends MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget))) {
|
||||
export class BrowserMod extends CameraMixin(
|
||||
MediaPlayerMixin(ScreenSaverMixin(ConnectionMixin(EventTarget)))
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
this.entity_id = deviceID.replace("-", "_");
|
||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "browser_mod",
|
||||
"version": "1.5.3",
|
||||
"version": "2.0.0b0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user