diff --git a/README.md b/README.md index 5aeba9e..f7a92f7 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ A Home Assistant integration to turn your browser into a controllable entity - a ## Example uses -- Make the camera feed from your front door pop up on the tablett in your kitchen when someone rings the doorbell. +- Make the camera feed from your front door pop up on the tablet in your kitchen when someone rings the doorbell. - Have a message pop up on every screen in the house when it's bedtime. - Make the browser on your workstation switch to a specific tab when the kitchen light is on after midnight - Play a TTS message on your work computer when the traffic sensor tells you it's time to go home. +- Display a full screen clock on your screen if no one's touched it for five minutes # Installation instructions @@ -49,38 +50,73 @@ Since the deviceID can be a bit hard to remember for devices you use often, you browser_mod: devices: 99980b13-dabc9563: - name: Arrakis + name: arrakis d2fc860c-16379d23: name: dashboard ``` -This binds the *aliases* `Arrakis` to `99980b13-dabc9563` and `dashboard` to `d2fc860c-16379d23`. +This binds the *aliases* `arrakis` to `99980b13-dabc9563` and `dashboard` to `d2fc860c-16379d23`. Note: Aliases must be unique. -## media\_player -Once `browser_mod` is installed, loading up your Home Assistant frontend on a new *device* will create a new `media_player` device. +## Entities +Once `browser_mod` is installed, loading up your Home Assistant frontend on a new *device* will create three or four new devices. -Any sound played on this media player will be played by the *device*. +- `sensor.` +- `media_player.` +- `light.` +- If you're using Fully Kiosk Browser `binary_sensor.` -The `media_player` entity also has some extra attributes presenting the current state of the *device*. +`` here will be the `deviceID` of the *device* but with the dash (`-`) replaced by an underscore (`_`). If you've defined an alias, it will be that instead. + +E.g: +Connecting your phone with `deviceID: ded3b4dc-abedd098` will create the entities `sensor.ded3b4dc_abedd098`, `media_player.ded3b4dc_abedd098` and `light.ded3b4dc_abedd098`. +Connecting with the computer named `Arrakis` above with `deviceID: 99980b13-dabc9563` will create the entities `sensor.arrakis`, `media_player.arrakis` and `light.arrakis`. + +### sensor + +The `sensor` will display the number of connected views (tabs/windows) of the device. Note that using multiple view isn't really recommended, and any action targeting a device will happen in the last loaded view. + +The sensor also has the following attributes: | attribute | content | | --- | --- | +| `type` | `browser_mod` | +| `last_seen` | The time when the *device* was last seen | +| `deviceID` | The deviceID of the *device*. | | `path` | The currently displayed path on the *device*. | | `visibility` | Whether the frontend is currently visible on the *device*. | | `userAgent` | The User Agent of the associated browser. | | `currentUser` | The user currently logged in on the *device*. | -| `blackout` | Whether the view on the *device* is currently blacked out (see below). | +| `fullyKiosk` | True if the *device* is a Fully Kiosk browser. Undefined otherwise. | +| `width` | The current width of the browser window in pixels. | +| `height` | The current height of the browser window in pixels. | -**NOTE: Because apple is apple; on iOS you need to touch the screen once after loading the frontend before any playback will work.** +### media\_player + +The `media_player` can be used to play sounds on the *device*. + +**NOTE: Because Apple is Apple; on iOS you need to touch the screen once after loading the frontend before any playback will work.** + +### light + +The `light` can be used to blackout the screen. +For Fully Kiosk Browser, the screen will actually turn off. +For other browsers, the interface will just be covered with black (the screen is still on, will have a visible glow in the dark, and you won't save any battery). + +### binary\_sensor + +The `binary_sensor` will only be available for Fully Kiosk Browser PRO *devices*. +It's state will be the state of the camera motion detector of the *device* (5 second cooldown). ## `browser_mod.command` service Call the `browser_mod.command` service to control your *device* in various ways. -All service calls have two parameters in common, `command` which is the command to execute, and `deviceID` which is a list of *devices* to execute the command on. If `deviceID` is omitted, the command will be executed on **all** currently connected *devices*. +All service calls have two parameters in common, `command` which is the command to execute, and `deviceID` which is a list of *devices* to execute the command on. If `deviceID` is omitted, the command will be executed on **all** currently connected *devices*. `deviceID` may also contain aliases. -`deviceID` may also contain aliases, and there's a special alias named `this` which will evaluate to the *device* from which a command was initiated (if from the frontend). +There is a special function that will replace the special alias `this` with the current deviceID in the list `deviceID` for any service call from the frontend. In the examples below it will be shown used for the `browser_mod.command` service, but it also works e.g for calling scripts from the frontend. + +All examples below are given in the syntax used for calling them from lovelace via e.g. an entity-button card with `tap_action:` set to `call-service`. If you call the service from a script or an automation, the syntax will be slightly different. ### debug @@ -90,7 +126,7 @@ service_data: command: debug ``` -Display a popup with the deviceID *and* a javascript allert with the deviceID on all connected *devices*. +Display a popup with the deviceID *and* a javascript alert with the deviceID on all connected *devices*. ### set-theme @@ -153,8 +189,9 @@ will display the specified `entities` card as a popup on the current device. ![popup-example](https://user-images.githubusercontent.com/1299821/60288984-a7cb6b00-9915-11e9-9322-324323a9ec6e.png) The optional parameter `large: true` will make the popup wider. -The optional parameter `style:` will apply css style options to the popup. +The optional parameter `style:` will apply CSS style options to the popup. The optional parameter `auto_close: true` will make the popup close automatically when the mouse is moved or a key is pressed on the keyboard. This also removes the header bar. +The optional parameter `time:` (only useable if `auto_close: true` is also set) will turn the popup into a "screensaver". See the `blackout` command below. Ex: ```yaml @@ -185,6 +222,8 @@ service_data: Will cover the entire window (or screen if in full screen mode) with black. Moving the mouse, touching the screen or pressing any key will restore the view. +The optional parameter `time:` will make the blackout turn on automatically after the specified number of seconds. It works kind of like a screensaver and will keep turning on until `blackout` is called again with `time: -1`. + Note: This will *not* turn off your screen backlight. Most screens will still emit light in a dark room. ### no-blackout @@ -224,7 +263,7 @@ The player card also displays the `entityID`. Click it to select, so you can cop # Fully Kiosk Browser If you are using a device running [Fully Kiosk Browser](https://www.ozerov.de/fully-kiosk-browser/) (PLUS version only) you will have access to a few more functions. -First of all the commands `blackout` and `no-blackout` will controll the devices screen directly. +First of all the commands `blackout` and `no-blackout` will control the devices screen directly. `no-blackout` also has an optional parameter `brightness` that can set the screen brightness between 0 and 255. Second, there are a few more attributes available @@ -257,7 +296,7 @@ This actually means it pretty much replaces `popup-card` as well. - This works even if the currently logged in user is not in the admin group. ### Does this replace lovelace-fullykiosk -It will, eventually. +Yes. You need the paid version, btw. ### Can the deviceID be used to track me across the internet @@ -269,7 +308,7 @@ Some of [my lovelace plugins](https://github.com/thomasloven/hass-config/wiki/My ### How do I run commands from /dev-service? -`/dev-service` requires json-formated service data. There's an explanation on the differences between yaml and json [here](http://thomasloven.com/blog/2018/08/YAML-For-Nonprogrammers/). +`/dev-service` requires json-formatted service data. There's an explanation on the differences between yaml and json [here](http://thomasloven.com/blog/2018/08/YAML-For-Nonprogrammers/). --- Buy Me A Coffee diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py index 1f88d95..17b2918 100644 --- a/custom_components/browser_mod/__init__.py +++ b/custom_components/browser_mod/__init__.py @@ -26,6 +26,7 @@ async def async_setup(hass, config): await hass.helpers.discovery.async_load_platform("media_player", DOMAIN, {}, config) await hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) + await hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config) await hass.helpers.discovery.async_load_platform("light", DOMAIN, {}, config) await setup_connection(hass, config) diff --git a/custom_components/browser_mod/binary_sensor.py b/custom_components/browser_mod/binary_sensor.py new file mode 100644 index 0000000..5240356 --- /dev/null +++ b/custom_components/browser_mod/binary_sensor.py @@ -0,0 +1,50 @@ +import logging +from datetime import datetime + +from homeassistant.const import STATE_UNAVAILABLE, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, STATE_ON, STATE_OFF +from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION + +from .helpers import setup_platform, BrowserModEntity + +PLATFORM = 'binary_sensor' + +async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + return setup_platform(hass, config, async_add_devices, PLATFORM, BrowserModSensor) + +class BrowserModSensor(BrowserModEntity): + domain = PLATFORM + + def __init__(self, hass, connection, deviceID, alias=None): + super().__init__(hass, connection, deviceID, alias) + self.last_seen = None + + def updated(self): + self.last_seen = datetime.now() + self.schedule_update_ha_state() + + @property + def state(self): + if not self.connection.connection: + return STATE_UNAVAILABLE + if self.data.get('motion', False): + return STATE_ON + return STATE_OFF + + @property + def is_on(self): + return not self.data.get('motion', False) + + + @property + def device_class(self): + return DEVICE_CLASS_MOTION + + @property + def device_state_attributes(self): + return { + "type": "browser_mod", + "last_seen": self.last_seen, + ATTR_BATTERY_LEVEL: self.data.get('battery', None), + ATTR_BATTERY_CHARGING: self.data.get('charging', None), + **self.data + } diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index fd29071..4dceec2 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -1,209 +1,56 @@ -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); -/******/ } -/******/ }; -/******/ -/******/ // define __esModule on exports -/******/ __webpack_require__.r = function(exports) { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ -/******/ // create a fake namespace object -/******/ // mode & 1: value is a module id, require it -/******/ // mode & 2: merge all properties of value into the ns -/******/ // mode & 4: return value when already ns object -/******/ // mode & 8|1: behave like require -/******/ __webpack_require__.t = function(value, mode) { -/******/ if(mode & 1) value = __webpack_require__(value); -/******/ if(mode & 8) return value; -/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; -/******/ var ns = Object.create(null); -/******/ __webpack_require__.r(ns); -/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); -/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); -/******/ return ns; -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = "./js/main.js"); -/******/ }) -/************************************************************************/ -/******/ ({ +!function(e){var t={};function i(o){if(t[o])return t[o].exports;var n=t[o]={i:o,l:!1,exports:{}};return e[o].call(n.exports,n,n.exports,i),n.l=!0,n.exports}i.m=e,i.c=t,i.d=function(e,t,o){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(o,n,function(t){return e[t]}.bind(null,n));return o},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t,i){"use strict";i.r(t);let o=function(){if(window.fully&&"function"==typeof fully.getDeviceId)return fully.getDeviceId();if(!localStorage["lovelace-player-device-id"]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);localStorage["lovelace-player-device-id"]=`${e()}${e()}-${e()}${e()}`}return localStorage["lovelace-player-device-id"]}();function n(e){return document.querySelector("home-assistant").provideHass(e)}function s(e,t,i=null){if((e=new Event(e,{bubbles:!0,cancelable:!1,composed:!0})).detail=t||{},i)i.dispatchEvent(e);else{var o=document.querySelector("home-assistant");(o=(o=(o=(o=(o=(o=(o=(o=(o=(o=(o=o&&o.shadowRoot)&&o.querySelector("home-assistant-main"))&&o.shadowRoot)&&o.querySelector("app-drawer-layout partial-panel-resolver"))&&o.shadowRoot||o)&&o.querySelector("ha-panel-lovelace"))&&o.shadowRoot)&&o.querySelector("hui-root"))&&o.shadowRoot)&&o.querySelector("ha-app-layout #view"))&&o.firstElementChild)&&o.dispatchEvent(e)}}const r="custom:";function a(e,t){const i=document.createElement("hui-error-card");return i.setConfig({type:"error",error:e,config:t}),i}function l(e,t){if(!t||"object"!=typeof t||!t.type)return a(`No ${e} type configured`,t);let i=t.type;if(i=i.startsWith(r)?i.substr(r.length):`hui-${i}-${e}`,customElements.get(i))return function(e,t){const i=document.createElement(e);try{i.setConfig(t)}catch(e){return a(e,t)}return i}(i,t);const o=a(`Custom element doesn't exist: ${i}.`,t);o.style.display="None";const n=setTimeout(()=>{o.style.display=""},2e3);return customElements.whenDefined(i).then(()=>{clearTimeout(n),s("ll-rebuild",{},o)}),o}function c(e,t=!1){s("hass-more-info",{entityId:e},document.querySelector("home-assistant"));const i=document.querySelector("home-assistant")._moreInfoEl;return i.large=t,i}const u=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),d=u.prototype.html,h=u.prototype.css;class p extends u{static get properties(){return{hass:{},config:{}}}setConfig(e){this._config=e,this.el?this.el.setConfig(e):this.el=this.create(e),this._hass&&(this.el.hass=this._hass),this.noHass&&n(this)}set config(e){this.setConfig(e)}set hass(e){this._hass=e,this.el&&(this.el.hass=e)}createRenderRoot(){return this}render(){return d`${this.el}`}}if(!customElements.get("card-maker")){class e extends p{create(e){return function(e){return l("card",e)}(e)}}customElements.define("card-maker",e)}if(!customElements.get("element-maker")){class e extends p{create(e){return function(e){return l("element",e)}(e)}}customElements.define("element-maker",e)}if(!customElements.get("entity-row-maker")){class e extends p{create(e){return function(e){const t=new Set(["call-service","divider","section","weblink"]);if(!e)return a("Invalid configuration given.",e);if("string"==typeof e&&(e={entity:e}),"object"!=typeof e||!e.entity&&!e.type)return a("Invalid configuration given.",e);const i=e.type||"default";if(t.has(i)||i.startsWith(r))return l("row",e);const o=e.entity.split(".",1)[0];return Object.assign(e,{type:{alert:"toggle",automation:"toggle",climate:"climate",cover:"cover",fan:"toggle",group:"group",input_boolean:"toggle",input_number:"input-number",input_select:"input-select",input_text:"input-text",light:"toggle",lock:"lock",media_player:"media-player",remote:"toggle",scene:"scene",script:"script",sensor:"sensor",timer:"timer",switch:"toggle",vacuum:"toggle",water_heater:"climate",input_datetime:"input-datetime"}[o]||"text"}),l("entity-row",e)}(e)}}customElements.define("entity-row-maker",e)}function m(e,t,i=!1,o=null,n=!1){c(Object.keys(document.querySelector("home-assistant").hass.states)[0]);const s=document.createElement("card-maker");s.noHass=!0,s.config=t;const r=document.createElement("div");r.innerHTML="\n \n ";const a=document.createElement("app-toolbar");a.innerHTML=`\n \n
\n ${e}\n
\n `;const l=document.createElement("div");l.classList.add("scrollable"),l.appendChild(s),n||r.appendChild(a),r.appendChild(l);const u=document.querySelector("home-assistant")._moreInfoEl;u.sizingTarget=l,u.large=i,u._page="none",u.shadowRoot.appendChild(r);let d={};if(o)for(var h in o)d[h]=u.style[h],u.style.setProperty(h,o[h]);return setTimeout(()=>{let e=setInterval(()=>{if(u.getAttribute("aria-hidden"))for(var t in clearInterval(e),r.parentNode.removeChild(r),d)d[t]?u.style.setProperty(t,d[t]):u.style.removeProperty(t)},100)},1e3),u}customElements.define("browser-player",class extends u{static get properties(){return{hass:{}}}setConfig(e){this._config=e}handleMute(e){window.browser_mod.mute({})}handleVolumeChange(e){const t=parseFloat(e.target.value);window.browser_mod.set_volume({volume_level:t})}handleMoreInfo(e){c(window.browser_mod.entity_id)}handlePlayPause(e){window.browser_mod.player.paused?window.browser_mod.play({}):window.browser_mod.pause({})}render(){const e=window.browser_mod.player;return d` + +
+ + -/***/ "../../../card-tools/card-maker.js": -/*!*********************************!*\ - !*** /card-tools/card-maker.js ***! - \*********************************/ -/*! no exports provided */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + ${"stopped"===window.browser_mod.player_state?d`
`:d` + + `} + +
-"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _lit_element_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./lit-element.js */ \"../../../card-tools/lit-element.js\");\n/* harmony import */ var _lovelace_element_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./lovelace-element.js */ \"../../../card-tools/lovelace-element.js\");\n/* harmony import */ var _hass_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./hass.js */ \"../../../card-tools/hass.js\");\n\n\n\n\nclass ThingMaker extends _lit_element_js__WEBPACK_IMPORTED_MODULE_0__[\"LitElement\"] {\n static get properties() {\n return {\n 'hass': {},\n 'config': {},\n };\n }\n setConfig(config) {\n this._config = config;\n if(!this.el)\n this.el = this.create(config);\n else\n this.el.setConfig(config);\n if(this._hass) this.el.hass = this._hass;\n if(this.noHass) Object(_hass_js__WEBPACK_IMPORTED_MODULE_2__[\"provideHass\"])(this);\n }\n set config(config) {\n this.setConfig(config);\n }\n set hass(hass) {\n this._hass = hass;\n if(this.el) this.el.hass = hass;\n }\n\n createRenderRoot() {\n return this;\n }\n render() {\n return _lit_element_js__WEBPACK_IMPORTED_MODULE_0__[\"html\"]`${this.el}`;\n }\n}\n\nif(!customElements.get(\"card-maker\")) {\n class CardMaker extends ThingMaker {\n create(config) {\n return Object(_lovelace_element_js__WEBPACK_IMPORTED_MODULE_1__[\"createCard\"])(config);\n }\n }\n customElements.define(\"card-maker\", CardMaker);\n}\n\nif(!customElements.get(\"element-maker\")) {\n class ElementMaker extends ThingMaker {\n create(config) {\n return Object(_lovelace_element_js__WEBPACK_IMPORTED_MODULE_1__[\"createElement\"])(config);\n }\n }\n customElements.define(\"element-maker\", ElementMaker);\n}\n\nif(!customElements.get(\"entity-row-maker\")) {\n class EntityRowMaker extends ThingMaker {\n create(config) {\n return Object(_lovelace_element_js__WEBPACK_IMPORTED_MODULE_1__[\"createEntityRow\"])(config);\n }\n }\n customElements.define(\"entity-row-maker\", EntityRowMaker);\n}\n\n\n//# sourceURL=webpack:////card-tools/card-maker.js?"); +
+ ${o} +
-/***/ }), - -/***/ "../../../card-tools/deviceId.js": -/*!*******************************!*\ - !*** /card-tools/deviceId.js ***! - \*******************************/ -/*! exports provided: deviceID */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"deviceID\", function() { return deviceID; });\nfunction _deviceID() {\n const ID_STORAGE_KEY = 'lovelace-player-device-id';\n if(window['fully'] && typeof fully.getDeviceId === \"function\")\n return fully.getDeviceId();\n if(!localStorage[ID_STORAGE_KEY])\n {\n const s4 = () => {\n return Math.floor((1+Math.random())*100000).toString(16).substring(1);\n }\n localStorage[ID_STORAGE_KEY] = `${s4()}${s4()}-${s4()}${s4()}`;\n }\n return localStorage[ID_STORAGE_KEY];\n};\n\nlet deviceID = _deviceID();\n\n\n//# sourceURL=webpack:////card-tools/deviceId.js?"); - -/***/ }), - -/***/ "../../../card-tools/event.js": -/*!****************************!*\ - !*** /card-tools/event.js ***! - \****************************/ -/*! exports provided: fireEvent */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"fireEvent\", function() { return fireEvent; });\nfunction fireEvent(ev, detail, entity=null) {\n ev = new Event(ev, {\n bubbles: true,\n cancelable: false,\n composed: true,\n });\n ev.detail = detail || {};\n if(entity) {\n entity.dispatchEvent(ev);\n } else {\n var root = document.querySelector(\"home-assistant\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"home-assistant-main\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"app-drawer-layout partial-panel-resolver\");\n root = root && root.shadowRoot || root;\n root = root && root.querySelector(\"ha-panel-lovelace\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"hui-root\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"ha-app-layout #view\");\n root = root && root.firstElementChild;\n if (root) root.dispatchEvent(ev);\n }\n}\n\n\n//# sourceURL=webpack:////card-tools/event.js?"); - -/***/ }), - -/***/ "../../../card-tools/hass.js": -/*!***************************!*\ - !*** /card-tools/hass.js ***! - \***************************/ -/*! exports provided: hass, provideHass, lovelace, lovelace_view */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"hass\", function() { return hass; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"provideHass\", function() { return provideHass; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"lovelace\", function() { return lovelace; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"lovelace_view\", function() { return lovelace_view; });\nfunction hass() {\n return document.querySelector('home-assistant').hass\n};\n\nfunction provideHass(element) {\n return document.querySelector(\"home-assistant\").provideHass(element);\n}\n\nfunction lovelace() {\n var root = document.querySelector(\"home-assistant\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"home-assistant-main\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"app-drawer-layout partial-panel-resolver\");\n root = root && root.shadowRoot || root;\n root = root && root.querySelector(\"ha-panel-lovelace\")\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"hui-root\")\n if (root) {\n var ll = root.lovelace\n ll.current_view = root.___curView;\n return ll;\n }\n return null;\n}\n\nfunction lovelace_view() {\n var root = document.querySelector(\"home-assistant\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"home-assistant-main\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"app-drawer-layout partial-panel-resolver\");\n root = root && root.shadowRoot || root;\n root = root && root.querySelector(\"ha-panel-lovelace\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"hui-root\");\n root = root && root.shadowRoot;\n root = root && root.querySelector(\"ha-app-layout #view\");\n root = root && root.firstElementChild;\n return root;\n}\n\n\n//# sourceURL=webpack:////card-tools/hass.js?"); - -/***/ }), - -/***/ "../../../card-tools/lit-element.js": -/*!**********************************!*\ - !*** /card-tools/lit-element.js ***! - \**********************************/ -/*! exports provided: LitElement, html, css */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LitElement\", function() { return LitElement; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"html\", function() { return html; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"css\", function() { return css; });\nconst LitElement = customElements.get('home-assistant-main') ? Object.getPrototypeOf(customElements.get('home-assistant-main')) : Object.getPrototypeOf(customElements.get('hui-view'));\n\nconst html = LitElement.prototype.html;\n\nconst css = LitElement.prototype.css;\n\n\n//# sourceURL=webpack:////card-tools/lit-element.js?"); - -/***/ }), - -/***/ "../../../card-tools/lovelace-element.js": -/*!***************************************!*\ - !*** /card-tools/lovelace-element.js ***! - \***************************************/ -/*! exports provided: CUSTOM_TYPE_PREFIX, DOMAINS_HIDE_MORE_INFO, createCard, createElement, createEntityRow */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"CUSTOM_TYPE_PREFIX\", function() { return CUSTOM_TYPE_PREFIX; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"DOMAINS_HIDE_MORE_INFO\", function() { return DOMAINS_HIDE_MORE_INFO; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"createCard\", function() { return createCard; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"createElement\", function() { return createElement; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"createEntityRow\", function() { return createEntityRow; });\n/* harmony import */ var _event_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./event.js */ \"../../../card-tools/event.js\");\n\n\nconst CUSTOM_TYPE_PREFIX = \"custom:\";\n\nconst DOMAINS_HIDE_MORE_INFO = [\n \"input_number\",\n \"input_select\",\n \"input_text\",\n \"scene\",\n \"weblink\",\n];\n\nfunction errorElement(error, config) {\n const el = document.createElement(\"hui-error-card\");\n el.setConfig({\n type: \"error\",\n error,\n config,\n });\n return el;\n}\n\nfunction _createElement(tag, config) {\n const el = document.createElement(tag);\n try {\n el.setConfig(config);\n } catch (err) {\n return errorElement(err, config);\n }\n return el;\n}\n\nfunction createLovelaceElement(thing, config) {\n if(!config || typeof config !== \"object\" || !config.type)\n return errorElement(`No ${thing} type configured`, config);\n\n let tag = config.type;\n if(tag.startsWith(CUSTOM_TYPE_PREFIX))\n tag = tag.substr(CUSTOM_TYPE_PREFIX.length);\n else\n tag = `hui-${tag}-${thing}`;\n\n if(customElements.get(tag))\n return _createElement(tag, config);\n\n const el = errorElement(`Custom element doesn't exist: ${tag}.`, config);\n el.style.display = \"None\";\n\n const timer = setTimeout(() => {\n el.style.display = \"\";\n }, 2000);\n\n customElements.whenDefined(tag).then(() => {\n clearTimeout(timer);\n Object(_event_js__WEBPACK_IMPORTED_MODULE_0__[\"fireEvent\"])(\"ll-rebuild\", {}, el);\n });\n\n return el;\n}\n\nfunction createCard(config) {\n return createLovelaceElement('card', config);\n}\nfunction createElement(config) {\n return createLovelaceElement('element', config);\n}\nfunction createEntityRow(config) {\n const SPECIAL_TYPES = new Set([\n \"call-service\",\n \"divider\",\n \"section\",\n \"weblink\",\n ]);\n const DEFAULT_ROWS = {\n alert: \"toggle\",\n automation: \"toggle\",\n climate: \"climate\",\n cover: \"cover\",\n fan: \"toggle\",\n group: \"group\",\n input_boolean: \"toggle\",\n input_number: \"input-number\",\n input_select: \"input-select\",\n input_text: \"input-text\",\n light: \"toggle\",\n lock: \"lock\",\n media_player: \"media-player\",\n remote: \"toggle\",\n scene: \"scene\",\n script: \"script\",\n sensor: \"sensor\",\n timer: \"timer\",\n switch: \"toggle\",\n vacuum: \"toggle\",\n water_heater: \"climate\",\n input_datetime: \"input-datetime\",\n };\n\n if(!config)\n return errorElement(\"Invalid configuration given.\", config);\n if(typeof config === \"string\")\n config = {entity: config};\n if(typeof config !== \"object\" || (!config.entity && !config.type))\n return errorElement(\"Invalid configuration given.\", config);\n\n const type = config.type || \"default\";\n if(SPECIAL_TYPES.has(type) || type.startsWith(CUSTOM_TYPE_PREFIX))\n return createLovelaceElement('row', config);\n\n const domain = config.entity.split(\".\", 1)[0];\n Object.assign(config, {type: DEFAULT_ROWS[domain] || \"text\"});\n\n return createLovelaceElement('entity-row', config);\n}\n\n\n//# sourceURL=webpack:////card-tools/lovelace-element.js?"); - -/***/ }), - -/***/ "../../../card-tools/more-info.js": -/*!********************************!*\ - !*** /card-tools/more-info.js ***! - \********************************/ -/*! exports provided: moreInfo */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"moreInfo\", function() { return moreInfo; });\n/* harmony import */ var _event_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./event.js */ \"../../../card-tools/event.js\");\n\n\nfunction moreInfo(entity, large=false) {\n Object(_event_js__WEBPACK_IMPORTED_MODULE_0__[\"fireEvent\"])(\"hass-more-info\", {entityId: entity}, document.querySelector(\"home-assistant\"));\n const el = document.querySelector(\"home-assistant\")._moreInfoEl;\n el.large = large;\n return el;\n}\n\n\n//# sourceURL=webpack:////card-tools/more-info.js?"); - -/***/ }), - -/***/ "../../../card-tools/popup.js": -/*!****************************!*\ - !*** /card-tools/popup.js ***! - \****************************/ -/*! exports provided: closePopUp, popUp */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"closePopUp\", function() { return closePopUp; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"popUp\", function() { return popUp; });\n/* harmony import */ var _hass_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hass.js */ \"../../../card-tools/hass.js\");\n/* harmony import */ var _event_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./event.js */ \"../../../card-tools/event.js\");\n/* harmony import */ var _lovelace_element_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./lovelace-element.js */ \"../../../card-tools/lovelace-element.js\");\n/* harmony import */ var _more_info_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./more-info.js */ \"../../../card-tools/more-info.js\");\n/* harmony import */ var _card_maker_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./card-maker.js */ \"../../../card-tools/card-maker.js\");\n\n\n\n\n\n\nfunction closePopUp() {\n const moreInfoEl = document.querySelector(\"home-assistant\")._moreInfoEl;\n if(moreInfoEl)\n moreInfoEl.close();\n}\n\nfunction popUp(title, card, large=false, style=null, fullscreen=false) {\n\n const dummy_entity = Object.keys(Object(_hass_js__WEBPACK_IMPORTED_MODULE_0__[\"hass\"])().states)[0];\n Object(_more_info_js__WEBPACK_IMPORTED_MODULE_3__[\"moreInfo\"])(dummy_entity);\n\n const content = document.createElement(\"card-maker\")\n content.noHass = true;\n content.config = card;\n\n const wrapper = document.createElement(\"div\");\n wrapper.innerHTML = `\n \n `;\n\n const header = document.createElement(\"app-toolbar\");\n header.innerHTML = `\n \n
\n ${title}\n
\n `;\n\n const scroll = document.createElement(\"div\");\n scroll.classList.add('scrollable');\n scroll.appendChild(content);\n\n if(!fullscreen) {\n wrapper.appendChild(header);\n }\n wrapper.appendChild(scroll);\n\n const moreInfoEl = document.querySelector(\"home-assistant\")._moreInfoEl;\n moreInfoEl.sizingTarget = scroll;\n moreInfoEl.large = large;\n moreInfoEl._page = \"none\";\n moreInfoEl.shadowRoot.appendChild(wrapper);\n\n let oldStyle = {};\n if(style) {\n for (var k in style) {\n oldStyle[k] = moreInfoEl.style[k];\n moreInfoEl.style.setProperty(k, style[k]);\n }\n }\n\n setTimeout(() => {\n let interval = setInterval(() => {\n if (moreInfoEl.getAttribute(\"aria-hidden\")) {\n clearInterval(interval);\n wrapper.parentNode.removeChild(wrapper);\n for (var k in oldStyle)\n if (oldStyle[k])\n moreInfoEl.style.setProperty(k, oldStyle[k]);\n else\n moreInfoEl.style.removeProperty(k);\n }\n }, 100);\n }, 1000);\n return moreInfoEl;\n}\n\n\n//# sourceURL=webpack:////card-tools/popup.js?"); - -/***/ }), - -/***/ "./js/browser-player.js": -/*!******************************!*\ - !*** ./js/browser-player.js ***! - \******************************/ -/*! no exports provided */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _card_tools_lit_element__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! /card-tools/lit-element */ \"../../../card-tools/lit-element.js\");\n/* harmony import */ var _card_tools_deviceId__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! /card-tools/deviceId */ \"../../../card-tools/deviceId.js\");\n/* harmony import */ var _card_tools_more_info__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! /card-tools/more-info */ \"../../../card-tools/more-info.js\");\n\n\n\n\nclass BrowserPlayer extends _card_tools_lit_element__WEBPACK_IMPORTED_MODULE_0__[\"LitElement\"] {\n\n static get properties() {\n return {\n hass: {},\n };\n }\n\n setConfig(config) {\n this._config = config;\n }\n handleMute(ev) {\n window.browser_mod.mute({});\n }\n handleVolumeChange(ev) {\n const vol = parseFloat(ev.target.value);\n window.browser_mod.set_volume({volume_level: vol});\n }\n handleMoreInfo(ev) {\n Object(_card_tools_more_info__WEBPACK_IMPORTED_MODULE_2__[\"moreInfo\"])(window.browser_mod.entity_id);\n }\n handlePlayPause(ev) {\n if (window.browser_mod.player.paused)\n window.browser_mod.play({});\n else\n window.browser_mod.pause({});\n }\n\n render() {\n const player = window.browser_mod.player;\n return _card_tools_lit_element__WEBPACK_IMPORTED_MODULE_0__[\"html\"]`\n \n
\n \n \n\n ${window.browser_mod.player_state === \"stopped\"\n ? _card_tools_lit_element__WEBPACK_IMPORTED_MODULE_0__[\"html\"]`
`\n : _card_tools_lit_element__WEBPACK_IMPORTED_MODULE_0__[\"html\"]`\n \n `}\n \n
\n\n
\n ${_card_tools_deviceId__WEBPACK_IMPORTED_MODULE_1__[\"deviceID\"]}\n
\n\n
\n `;\n }\n\n static get styles() {\n return _card_tools_lit_element__WEBPACK_IMPORTED_MODULE_0__[\"css\"]`\n paper-icon-button[highlight] {\n color: var(--accent-color);\n }\n .card-content {\n display: flex;\n justify-content: center;\n }\n .placeholder {\n width: 24px;\n padding: 8px;\n }\n .device-id {\n opacity: 0.7;\n font-size: xx-small;\n margin-top: -10px;\n user-select: all;\n -webkit-user-select: all;\n -moz-user-select: all;\n -ms-user-select: all;\n }\n `\n }\n\n}\n\ncustomElements.define(\"browser-player\", BrowserPlayer);\n\n\n//# sourceURL=webpack:///./js/browser-player.js?"); - -/***/ }), - -/***/ "./js/main.js": -/*!********************!*\ - !*** ./js/main.js ***! - \********************/ -/*! no exports provided */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _card_tools_deviceId__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! /card-tools/deviceId */ \"../../../card-tools/deviceId.js\");\n/* harmony import */ var _card_tools_hass__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! /card-tools/hass */ \"../../../card-tools/hass.js\");\n/* harmony import */ var _card_tools_popup__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! /card-tools/popup */ \"../../../card-tools/popup.js\");\n/* harmony import */ var _card_tools_event__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! /card-tools/event */ \"../../../card-tools/event.js\");\n/* harmony import */ var _card_tools_more_info_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! /card-tools/more-info.js */ \"../../../card-tools/more-info.js\");\n/* harmony import */ var _browser_player__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./browser-player */ \"./js/browser-player.js\");\n\n\n\n\n\n\n\nclass BrowserMod {\n\n set hass(hass) {\n if(!hass) return;\n this._hass = hass;\n if(this.hassPatched) return;\n const callService = hass.callService;\n const newCallService = (domain, service, serviceData) => {\n if(domain === \"browser_mod\" && service === \"command\") {\n if(serviceData.deviceID) {\n const index = serviceData.deviceID.indexOf('this');\n if(index !== -1)\n serviceData.deviceID[index] = _card_tools_deviceId__WEBPACK_IMPORTED_MODULE_0__[\"deviceID\"];\n }\n }\n return callService(domain, service, serviceData);\n };\n hass.callService = newCallService;\n\n this.hassPatched = true;\n document.querySelector(\"home-assistant\").hassChanged(hass, hass);\n }\n\n playOnce(ev) {\n if(window.browser_mod.playedOnce) return;\n window.browser_mod.player.play();\n window.browser_mod.playedOnce = true;\n }\n\n constructor() {\n window.hassConnection.then((conn) => this.connect(conn.conn));\n this.player = new Audio();\n this.playedOnce = false;\n\n this.autoclose_popup_active = false;\n\n const updater = this.update.bind(this);\n this.player.addEventListener(\"ended\", updater);\n this.player.addEventListener(\"play\", updater);\n this.player.addEventListener(\"pause\", updater);\n this.player.addEventListener(\"volumechange\", updater);\n document.addEventListener(\"visibilitychange\", updater);\n window.addEventListener(\"location-changed\", updater);\n window.addEventListener(\"click\", this.playOnce);\n window.addEventListener(\"mousemove\", this.no_blackout.bind(this));\n window.addEventListener(\"mousedown\", this.no_blackout.bind(this));\n window.addEventListener(\"keydown\", this.no_blackout.bind(this));\n window.addEventListener(\"touchstart\", this.no_blackout.bind(this));\n Object(_card_tools_hass__WEBPACK_IMPORTED_MODULE_1__[\"provideHass\"])(this);\n\n if(window.fully)\n {\n this._fullyMotion = false;\n this._motionTimeout = undefined;\n fully.bind('screenOn', 'browser_mod.update();');\n fully.bind('screenOff', 'browser_mod.update();');\n fully.bind('onMotion', 'browser_mod.fullyMotion();');\n }\n\n this._blackout = document.createElement(\"div\");\n this._blackout.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n padding: 0;\n margin: 0;\n width: 100%;\n height: 100%;\n background: black;\n visibility: hidden;\n `;\n document.body.appendChild(this._blackout);\n }\n\n connect(conn) {\n this.conn = conn\n conn.subscribeMessage((msg) => this.callback(msg), {\n type: 'browser_mod/connect',\n deviceID: _card_tools_deviceId__WEBPACK_IMPORTED_MODULE_0__[\"deviceID\"],\n fully: window.fully ? true : undefined,\n });\n }\n\n callback(msg) {\n switch (msg.command) {\n case \"update\":\n this.update(msg);\n break;\n\n case \"debug\":\n this.debug(msg);\n break;\n\n case \"play\":\n this.play(msg);\n break;\n case \"pause\":\n this.pause(msg);\n break;\n case \"stop\":\n this.stop(msg);\n break;\n case \"set_volume\":\n this.set_volume(msg);\n break;\n case \"mute\":\n this.mute(msg);\n break;\n\n case \"popup\":\n this.popup(msg);\n break;\n case \"close-popup\":\n this.close_popup(msg);\n break;\n case \"navigate\":\n this.navigate(msg);\n break;\n case \"more-info\":\n this.more_info(msg);\n break;\n case \"set-theme\":\n this.set_theme(msg);\n break;\n\n case \"lovelace-reload\":\n this.lovelace_reload(msg);\n break;\n\n case \"blackout\":\n this.blackout(msg);\n break;\n case \"no-blackout\":\n this.no_blackout(msg);\n break;\n }\n }\n\n get player_state() {\n if (!this.player.src) return \"stopped\";\n if (this.player.ended) return \"stopped\";\n if (this.player.paused) return \"paused\";\n return \"playing\";\n }\n\n debug(msg) {\n Object(_card_tools_popup__WEBPACK_IMPORTED_MODULE_2__[\"popUp\"])(`deviceID`, {type: \"markdown\", content: `# ${_card_tools_deviceId__WEBPACK_IMPORTED_MODULE_0__[\"deviceID\"]}`})\n alert(_card_tools_deviceId__WEBPACK_IMPORTED_MODULE_0__[\"deviceID\"]);\n }\n\n play(msg) {\n const src = msg.media_content_id;\n if(src)\n this.player.src = src;\n this.player.play();\n }\n pause(msg) {\n this.player.pause();\n }\n stop(msg) {\n this.player.pause();\n this.player.src = null;\n }\n set_volume(msg) {\n if (msg.volume_level === undefined) return;\n this.player.volume = msg.volume_level;\n }\n mute(msg) {\n if (msg.mute === undefined)\n msg.mute = !this.player.muted;\n this.player.muted = Boolean(msg.mute)\n }\n\n popup(msg){\n if(!msg.title && !msg.auto_close) return;\n if(!msg.card) return;\n Object(_card_tools_popup__WEBPACK_IMPORTED_MODULE_2__[\"popUp\"])(msg.title, msg.card, msg.large, msg.style, msg.auto_close);\n if(msg.auto_close)\n this.autoclose_popup_active = true;\n }\n close_popup(msg){\n this.autoclose_popup_active = false;\n Object(_card_tools_popup__WEBPACK_IMPORTED_MODULE_2__[\"closePopUp\"])();\n }\n navigate(msg){\n if(!msg.navigation_path) return;\n history.pushState(null, \"\", msg.navigation_path);\n Object(_card_tools_event__WEBPACK_IMPORTED_MODULE_3__[\"fireEvent\"])(\"location-changed\", {}, document.querySelector(\"home-assistant\"));\n }\n more_info(msg){\n if(!msg.entity_id) return;\n Object(_card_tools_more_info_js__WEBPACK_IMPORTED_MODULE_4__[\"moreInfo\"])(msg.entity_id, msg.large);\n }\n set_theme(msg){\n if(!msg.theme) msg.theme = \"default\";\n Object(_card_tools_event__WEBPACK_IMPORTED_MODULE_3__[\"fireEvent\"])(\"settheme\", msg.theme, document.querySelector(\"home-assistant\"));\n }\n\n lovelace_reload(msg) {\n const ll = Object(_card_tools_hass__WEBPACK_IMPORTED_MODULE_1__[\"lovelace_view\"])();\n if (ll)\n Object(_card_tools_event__WEBPACK_IMPORTED_MODULE_3__[\"fireEvent\"])(\"config-refresh\", {}, ll);\n }\n\n blackout(msg){\n if (window.fully)\n {\n fully.turnScreenOff();\n } else {\n this._blackout.style.visibility = \"visible\";\n }\n this.update();\n }\n no_blackout(msg){\n if(this.autoclose_popup_active)\n return this.close_popup();\n if (window.fully)\n {\n if (!fully.getScreenOn())\n fully.turnScreenOn();\n if (msg.brightness)\n fully.setScreenBrightness(msg.brightness);\n this.update();\n } else {\n if(this._blackout.style.visibility !== \"hidden\") {\n this._blackout.style.visibility = \"hidden\";\n this.update();\n }\n }\n }\n is_blackout(){\n if (window.fully)\n return !fully.getScreenOn();\n return Boolean(this._blackout.style.visibility === \"visible\")\n }\n\n fullyMotion() {\n this._fullyMotion = true;\n clearTimeout(this._motionTimeout);\n this._motionTimeout = setTimeout(() => {\n this._fullyMotion = false;\n this.update();\n }, 5000);\n this.update();\n }\n\n\n update(msg=null) {\n if(!this.conn) return;\n\n if(msg)\n if(msg.entity_id)\n this.entity_id = msg.entity_id;\n\n this.conn.sendMessage({\n type: 'browser_mod/update',\n deviceID: _card_tools_deviceId__WEBPACK_IMPORTED_MODULE_0__[\"deviceID\"],\n data: {\n browser: {\n path: window.location.pathname,\n visibility: document.visibilityState,\n userAgent: navigator.userAgent,\n currentUser: this._hass && this._hass.user && this._hass.user.name,\n fullyKiosk: window.fully ? true : undefined,\n },\n player: {\n volume: this.player.volume,\n muted: this.player.muted,\n src: this.player.src,\n state: this.player_state,\n },\n screen: {\n blackout: this.is_blackout(),\n brightness: window.fully ? fully.getScreenBrightness() : undefined,\n },\n fully: window.fully ? {\n battery: window.fully ? fully.getBatteryLevel() : undefined,\n charging: window.fully ? fully.isPlugged(): undefined,\n motion: window.fully ? this._fullyMotion : undefined,\n } : undefined,\n },\n });\n\n }\n\n}\n\nwindow.browser_mod = new BrowserMod();\n\n\n\n//# sourceURL=webpack:///./js/main.js?"); - -/***/ }) - -/******/ }); \ No newline at end of file +
+ `}static get styles(){return h` + paper-icon-button[highlight] { + color: var(--accent-color); + } + .card-content { + display: flex; + justify-content: center; + } + .placeholder { + width: 24px; + padding: 8px; + } + .device-id { + opacity: 0.7; + font-size: xx-small; + margin-top: -10px; + user-select: all; + -webkit-user-select: all; + -moz-user-select: all; + -ms-user-select: all; + } + `}});window.browser_mod=new class{set hass(e){if(!e)return;if(this._hass=e,this.hassPatched)return;const t=e.callService;e.callService=(e,i,n)=>{if(n&&n.deviceID){const e=n.deviceID.indexOf("this");-1!==e&&(n.deviceID[e]=o)}return t(e,i,n)},this.hassPatched=!0,document.querySelector("home-assistant").hassChanged(e,e)}playOnce(e){window.browser_mod.playedOnce||(window.browser_mod.player.play(),window.browser_mod.playedOnce=!0)}constructor(){window.hassConnection.then(e=>this.connect(e.conn)),this.player=new Audio,this.playedOnce=!1,this.autoclose_popup_active=!1;const e=this.update.bind(this);this.player.addEventListener("ended",e),this.player.addEventListener("play",e),this.player.addEventListener("pause",e),this.player.addEventListener("volumechange",e),document.addEventListener("visibilitychange",e),window.addEventListener("location-changed",e),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)),n(this),window.fully&&(this._fullyMotion=!1,this._motionTimeout=void 0,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=void 0,this._screenSaverTimer=void 0,this._screenSaverTime=0,this._blackout=document.createElement("div"),this._blackout.style.cssText="\n position: fixed;\n left: 0;\n top: 0;\n padding: 0;\n margin: 0;\n width: 100%;\n height: 100%;\n background: black;\n visibility: hidden;\n ",document.body.appendChild(this._blackout)}connect(e){this.conn=e,e.subscribeMessage(e=>this.callback(e),{type:"browser_mod/connect",deviceID:o})}callback(e){switch(e.command){case"update":this.update(e);break;case"debug":this.debug(e);break;case"play":this.play(e);break;case"pause":this.pause(e);break;case"stop":this.stop(e);break;case"set_volume":this.set_volume(e);break;case"mute":this.mute(e);break;case"popup":this.popup(e);break;case"close-popup":this.close_popup(e);break;case"navigate":this.navigate(e);break;case"more-info":this.more_info(e);break;case"set-theme":this.set_theme(e);break;case"lovelace-reload":this.lovelace_reload(e);break;case"blackout":this.blackout(e);break;case"no-blackout":this.no_blackout(e)}}get player_state(){return this.player.src?this.player.ended?"stopped":this.player.paused?"paused":"playing":"stopped"}debug(e){m("deviceID",{type:"markdown",content:`# ${o}`}),alert(o)}play(e){const t=e.media_content_id;t&&(this.player.src=t),this.player.play()}pause(e){this.player.pause()}stop(e){this.player.pause(),this.player.src=null}set_volume(e){void 0!==e.volume_level&&(this.player.volume=e.volume_level)}mute(e){void 0===e.mute&&(e.mute=!this.player.muted),this.player.muted=Boolean(e.mute)}popup(e){if(!e.title&&!e.auto_close)return;if(!e.card)return;const t=()=>{m(e.title,e.card,e.large,e.style,e.auto_close),auto_close&&(this.autoclose_popup_active=!0)};if(e.auto_close&&e.time){if(e.time=parseInt(e.time),-1==e.time)return clearTimeout(this._screenSaverTimer),void(this._screenSaverTime=0);this._screenSaverTime=1e3*e.time,this._screenSaver=t,this._screenSaverTimer=setTimeout(this._screenSaver,this._screenSaverTime)}else t()}close_popup(e){this.autoclose_popup_active=!1,function(){const e=document.querySelector("home-assistant")._moreInfoEl;e&&e.close()}(),this._screenSaverTime&&(this._screenSaverTimer=setTimeout(this._screenSaver,this._screenSaverTime))}navigate(e){e.navigation_path&&(history.pushState(null,"",e.navigation_path),s("location-changed",{},document.querySelector("home-assistant")))}more_info(e){e.entity_id&&c(e.entity_id,e.large)}set_theme(e){e.theme||(e.theme="default"),s("settheme",e.theme,document.querySelector("home-assistant"))}lovelace_reload(e){const t=i=(i=(i=(i=(i=(i=(i=(i=(i=(i=(i=(i=document.querySelector("home-assistant"))&&i.shadowRoot)&&i.querySelector("home-assistant-main"))&&i.shadowRoot)&&i.querySelector("app-drawer-layout partial-panel-resolver"))&&i.shadowRoot||i)&&i.querySelector("ha-panel-lovelace"))&&i.shadowRoot)&&i.querySelector("hui-root"))&&i.shadowRoot)&&i.querySelector("ha-app-layout #view"))&&i.firstElementChild;var i;t&&s("config-refresh",{},t)}blackout(e){const t=()=>{window.fully?fully.turnScreenOff():this._blackout.style.visibility="visible",this.update()};if(e.time){if(e.time=parseInt(e.time),-1==e.time)return clearTimeout(this._screenSaverTimer),void(this._screenSaverTime=0);this._screenSaverTime=1e3*e.time,this._screenSaver=t,this._screenSaverTimer=setTimeout(this._screenSaver,this._screenSaverTime)}else t()}no_blackout(e){if(clearTimeout(this._screenSaverTimer),this.autoclose_popup_active)return this.close_popup();window.fully?(fully.getScreenOn()||fully.turnScreenOn(),e.brightness&&fully.setScreenBrightness(e.brightness),this.update()):"hidden"!==this._blackout.style.visibility&&(this._blackout.style.visibility="hidden",this.update()),this._screenSaverTime&&(this._screenSaverTimer=setTimeout(this._screenSaver,this._screenSaverTime))}is_blackout(){return window.fully?!fully.getScreenOn():Boolean("visible"===this._blackout.style.visibility)}fullyMotion(){this._fullyMotion=!0,clearTimeout(this._motionTimeout),this._motionTimeout=setTimeout(()=>{this._fullyMotion=!1,this.update()},5e3),this.update()}update(e=null){this.conn&&(e&&e.entity_id&&(this.entity_id=e.entity_id),this.conn.sendMessage({type:"browser_mod/update",deviceID:o,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||void 0,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():void 0},fully:window.fully?{battery:window.fully?fully.getBatteryLevel():void 0,charging:window.fully?fully.isPlugged():void 0,motion:window.fully?this._fullyMotion:void 0}:void 0}}))}}}]); \ No newline at end of file diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index 0f29a01..37e5c76 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -10,23 +10,18 @@ from .helpers import get_devices, create_entity _LOGGER = logging.getLogger(__name__) async def setup_connection(hass, config): - _LOGGER.error("--------------------") - _LOGGER.error("Setting up BM connection") @websocket_command({ vol.Required("type"): WS_CONNECT, vol.Required("deviceID"): str, }) def handle_connect(hass, connection, msg): - _LOGGER.error("--------------------") - _LOGGER.error("CONNECTING BM") deviceID = msg["deviceID"] device = get_devices(hass).get(deviceID, BrowserModConnection(hass, deviceID)) device.connect(connection, msg["id"]) get_devices(hass)[deviceID] = device - _LOGGER.error("DONE") connection.send_message(result_message(msg["id"])) @websocket_command({ @@ -35,9 +30,6 @@ async def setup_connection(hass, config): vol.Optional("data"): dict, }) def handle_update( hass, connection, msg): - _LOGGER.error("--------------------") - _LOGGER.error("UPDATING BM") - _LOGGER.error(msg) devices = get_devices(hass) deviceID = msg["deviceID"] if deviceID in devices: @@ -56,11 +48,10 @@ class BrowserModConnection: self.media_player = None self.screen = None self.sensor = None + self.fully = None def connect(self, connection, cid): self.connection.append((connection, cid)) - _LOGGER.error("********************") - _LOGGER.error("Connected %s", self.deviceID) self.send("update") def disconnect(): @@ -77,15 +68,6 @@ class BrowserModConnection: })) def update(self, data): - _LOGGER.error("********************") - _LOGGER.error("Got update %s for %s", data, self.deviceID) - if data.get('player'): - self.media_player = self.media_player or create_entity( - self.hass, - 'media_player', - self.deviceID, - self) - self.media_player.data = data.get('player') if data.get('browser'): self.sensor = self.sensor or create_entity( self.hass, @@ -93,6 +75,15 @@ class BrowserModConnection: self.deviceID, self) self.sensor.data = data.get('browser') + + if data.get('player'): + self.media_player = self.media_player or create_entity( + self.hass, + 'media_player', + self.deviceID, + self) + self.media_player.data = data.get('player') + if data.get('screen'): self.screen = self.screen or create_entity( self.hass, @@ -101,3 +92,11 @@ class BrowserModConnection: self) self.screen.data = data.get('screen') + if data.get('fully'): + self.fully = self.fully or create_entity( + self.hass, + 'binary_sensor', + self.deviceID, + self) + self.fully.data = data.get('fully') + diff --git a/custom_components/browser_mod/helpers.py b/custom_components/browser_mod/helpers.py index 18d9df1..cba018a 100644 --- a/custom_components/browser_mod/helpers.py +++ b/custom_components/browser_mod/helpers.py @@ -16,8 +16,6 @@ def get_alias(hass, deviceID): return None def create_entity(hass, platform, deviceID, connection): - _LOGGER.error("********************") - _LOGGER.error("Creating %s for %s", platform, deviceID) adder = hass.data[DOMAIN][DATA_ADDERS][platform] entity = adder(hass, deviceID, connection, get_alias(hass, deviceID)) return entity diff --git a/custom_components/browser_mod/media_player.py b/custom_components/browser_mod/media_player.py index ae74d26..6a0fcaa 100644 --- a/custom_components/browser_mod/media_player.py +++ b/custom_components/browser_mod/media_player.py @@ -36,6 +36,7 @@ class BrowserModPlayer(MediaPlayerDevice, BrowserModEntity): def device_state_attributes(self): return { "type": "browser_mod", + "deviceID": self.deviceID, } @property diff --git a/custom_components/browser_mod/sensor.py b/custom_components/browser_mod/sensor.py index 94dff51..71c9a07 100644 --- a/custom_components/browser_mod/sensor.py +++ b/custom_components/browser_mod/sensor.py @@ -32,5 +32,6 @@ class BrowserModSensor(BrowserModEntity): return { "type": "browser_mod", "last_seen": self.last_seen, + "deviceID": self.deviceID, **self.data } diff --git a/custom_components/browser_mod/service.py b/custom_components/browser_mod/service.py index 0bbd814..3e50a5a 100644 --- a/custom_components/browser_mod/service.py +++ b/custom_components/browser_mod/service.py @@ -19,6 +19,6 @@ def setup_service(hass): for t in targets: if t in devices: - devices[t].ws_send(command, **data) + devices[t].send(command, **data) hass.services.async_register(DOMAIN, 'command', handle_command) diff --git a/info.md b/info.md index 8231635..ae080f9 100644 --- a/info.md +++ b/info.md @@ -5,11 +5,11 @@ A Home Assistant integration to turn your browser into a controllable entity - a ## Example uses -- Make the camera feed from your front door pop up on the tablett in your kitchen when someone rings the doorbell. +- Make the camera feed from your front door pop up on the tablet in your kitchen when someone rings the doorbell. - Have a message pop up on every screen in the house when it's bedtime. - Make the browser on your workstation switch to a specific tab when the kitchen light is on after midnight - Play a TTS message on your work computer when the traffic sensor tells you it's time to go home. -- Make the screen on your tablett go black during the night, but wake up when you touch it. +- Make the screen on your tablet go black during the night, but wake up when you touch it. ### See [README](https://github.com/thomasloven/hass-browser_mod/blob/master/README.md) for usage instructions diff --git a/js/main.js b/js/main.js index 558387f..262468b 100644 --- a/js/main.js +++ b/js/main.js @@ -13,12 +13,10 @@ class BrowserMod { if(this.hassPatched) return; const callService = hass.callService; const newCallService = (domain, service, serviceData) => { - if(domain === "browser_mod" && service === "command") { - if(serviceData.deviceID) { - const index = serviceData.deviceID.indexOf('this'); - if(index !== -1) - serviceData.deviceID[index] = deviceID; - } + if(serviceData && serviceData.deviceID) { + const index = serviceData.deviceID.indexOf('this'); + if(index !== -1) + serviceData.deviceID[index] = deviceID; } return callService(domain, service, serviceData); }; @@ -61,9 +59,18 @@ class BrowserMod { 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; @@ -84,7 +91,6 @@ class BrowserMod { conn.subscribeMessage((msg) => this.callback(msg), { type: 'browser_mod/connect', deviceID: deviceID, - fully: window.fully ? true : undefined, }); } @@ -181,13 +187,31 @@ class BrowserMod { popup(msg){ if(!msg.title && !msg.auto_close) return; if(!msg.card) return; - popUp(msg.title, msg.card, msg.large, msg.style, msg.auto_close); - if(msg.auto_close) - this.autoclose_popup_active = true; + const fn = () => { + popUp(msg.title, msg.card, msg.large, msg.style, msg.auto_close); + if(auto_close) + this.autoclose_popup_active = true; + }; + if(msg.auto_close && msg.time) { + msg.time = parseInt(msg.time) + if(msg.time == -1) { + clearTimeout(this._screenSaverTimer); + this._screenSaverTime = 0; + return; + } + this._screenSaverTime = msg.time * 1000; + this._screenSaver = fn; + this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime) + } else { + fn(); + } } close_popup(msg){ this.autoclose_popup_active = false; closePopUp(); + if(this._screenSaverTime) { + this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime) + } } navigate(msg){ if(!msg.navigation_path) return; @@ -210,15 +234,32 @@ class BrowserMod { } blackout(msg){ - if (window.fully) - { - fully.turnScreenOff(); + const fn = () => { + if (window.fully) + { + fully.turnScreenOff(); + } else { + this._blackout.style.visibility = "visible"; + } + this.update(); + }; + if(msg.time) { + msg.time = parseInt(msg.time) + if(msg.time == -1) { + clearTimeout(this._screenSaverTimer); + this._screenSaverTime = 0; + return; + } + this._screenSaverTime = msg.time * 1000; + this._screenSaver = fn; + this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime) } else { - this._blackout.style.visibility = "visible"; + fn(); } - this.update(); } no_blackout(msg){ + + clearTimeout(this._screenSaverTimer); if(this.autoclose_popup_active) return this.close_popup(); if (window.fully) @@ -234,6 +275,9 @@ class BrowserMod { this.update(); } } + if(this._screenSaverTime) { + this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime) + } } is_blackout(){ if (window.fully) @@ -269,6 +313,8 @@ class BrowserMod { 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,