diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..c79eac8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,45 @@ +--- +name: Bug report +about: For reporting bugs or unexpected behavior +title: '' +labels: '' +assignees: '' +--- + +My Home Assistant version: 0.XX.X + +What I am doing: + + +What I expected to happen: + + +What happened instead: + + +**Minimal** steps to reproduce: + + +```yaml +# The least ammount of code possible to reproduce my error + + + +# End of code +``` + + +Error messages from the browser console: + + +--- + +**By putting an X in the boxes ([ ]) below, I indicate that I:** + +- [ ] Understand that this is a channel for reporting bugs, not a support forum (https://community.home-assistant.io/). + +- [ ] Have made sure I am using the latest version of the plugin. + +- [ ] Have followed the troubleshooting steps of the "Common Problems" section of https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins. + +- [ ] Understand that leaving one or more boxes unticked or failure to follow the template above may increase the time required to handle my bug-report, or cause it to be closed without further action. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..e0f987c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: For suggesting new features +title: '' +labels: 'feature-request' +assignees: '' +--- diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..0d23789 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,9 @@ +daysUntilStale: 60 +daysUntilClose: 7 +exemptLabels: + - pinned + - feature-request +staleLabel: stale +markComment: > + This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. +closeComment: false diff --git a/.gitignore b/.gitignore index f728ed2..724a959 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/ **/__pycache__/ +.vscode +.env diff --git a/README.md b/README.md index 85d4b8f..42762c7 100644 --- a/README.md +++ b/README.md @@ -72,10 +72,14 @@ This binds the *aliases* `arrakis` to `99980b13-dabc9563` and `dashboard` to `d2 Note: Aliases must be unique. -#### Experimental: Custom deviceID +#### Changing deviceID +You can change the deviceID of your device by adding a `browser-player` card to your lovelace interface and clicking the deviceID at the bottom of the card. Set it to `clear` to generate a new random one. + You can also set a deviceID by adding `?deviceID=mydeviceID` to the end of the URL you're using to access Home Assistant. Be careful - I have no idea what could happen if several devices were to have the same ID. Use `?deviceID=clear` to generate a new random one. +**Take care to avoid deviceID collissions. There's no telling what could happen if more devices share the same ID.** + ### Prefix You can add a custom prefix to all entity ids in `configuration.yaml`: @@ -137,6 +141,8 @@ The sensor also has the following attributes: | `fullyKiosk` | True if the *device* is a Fully Kiosk browser. Undefined otherwise. | | `width` | The current width of the browser window in pixels. | | `height` | The current height of the browser window in pixels. | +| `battery_level` | The current battery level of your device - if supported | +| `charging` | The current charging state of your device - if supported | ### media\_player @@ -150,7 +156,7 @@ The `light` can be used to blackout the screen. For Fully Kiosk Browser, the screen will actually turn off. For other browsers, the interface will just be covered with black (the screen is still on, will have a visible glow in the dark, and you won't save any battery). -### camera (EXPERIMENTAL) +### camera For security and UX reasons, the camera must be enabled manually on a device by device basis. @@ -179,7 +185,7 @@ It's state will be the state of the camera motion detector of the *device* (5 se `browser_mod` registers a number of services. -All service calls have one parameter in common; `deviceID` which is a list of *devices* to execute the comand on. If `deviceID` is omitted, the command will be executed on **all** currenctly connected *devices*. `deviceID` may also contain aliases. +All service calls have one parameter in common; `deviceID` which is a list of *devices* to execute the comand on. If `deviceID` is omitted, the command will be executed on **all** currently connected *devices*. `deviceID` may also contain aliases. If a service is called from the frontend (e.g. by using the `call-service` tap action), a value of `this` in the `deviceID` list will be replaced with the ID of the *device* the call was made from. Alternatively, `deviceID: this` will also work. @@ -333,7 +339,7 @@ Second, there are a few more attributes available | --- | --- | | `fullyKiosk` | True. | | `brightness` | The current screen brightness. | -| `battery` | The current charge percentage of the devices battery. | +| `battery_level` | The current charge percentage of the devices battery. | | `charging` | Whether the battery is currently charging. | | `motion` | Whether the devices camera has detected any motion in the last five seconds. | diff --git a/custom_components/browser_mod/__init__.py b/custom_components/browser_mod/__init__.py index 6cb7443..a3440d6 100644 --- a/custom_components/browser_mod/__init__.py +++ b/custom_components/browser_mod/__init__.py @@ -3,39 +3,50 @@ import logging from .mod_view import setup_view from .connection import setup_connection from .service import setup_service -from .const import DOMAIN, DATA_DEVICES, DATA_ALIASES, DATA_ADDERS, CONFIG_DEVICES, DATA_CONFIG - +from .const import ( + DOMAIN, + DATA_DEVICES, + DATA_ALIASES, + DATA_ADDERS, + CONFIG_DEVICES, + DATA_CONFIG, + DATA_SETUP_COMPLETE, +) _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config): - await setup_connection(hass, config) - setup_view(hass) +async def async_setup(hass, config): aliases = {} for d in config[DOMAIN].get(CONFIG_DEVICES, {}): name = config[DOMAIN][CONFIG_DEVICES][d].get("name", None) if name: - aliases[name] = d.replace('_','-') + aliases[name] = d.replace('_', '-') hass.data[DOMAIN] = { DATA_DEVICES: {}, DATA_ALIASES: aliases, DATA_ADDERS: {}, DATA_CONFIG: config[DOMAIN], + DATA_SETUP_COMPLETE: False, } - await hass.helpers.discovery.async_load_platform("media_player", DOMAIN, {}, config) - await hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) - await hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config) - await hass.helpers.discovery.async_load_platform("light", DOMAIN, {}, config) - await hass.helpers.discovery.async_load_platform("camera", DOMAIN, {}, config) + await setup_connection(hass, config) + setup_view(hass) + async_load_platform = hass.helpers.discovery.async_load_platform + await async_load_platform("media_player", DOMAIN, {}, config) + await async_load_platform("sensor", DOMAIN, {}, config) + await async_load_platform("binary_sensor", DOMAIN, {}, config) + await async_load_platform("light", DOMAIN, {}, config) + await async_load_platform("camera", DOMAIN, {}, config) await setup_service(hass) + hass.data[DOMAIN][DATA_SETUP_COMPLETE] = True + + for device in hass.data[DOMAIN][DATA_DEVICES].values(): + device.trigger_update() + return True - - - diff --git a/custom_components/browser_mod/browser_mod.js b/custom_components/browser_mod/browser_mod.js index 13c5fb5..263c7ac 100644 --- a/custom_components/browser_mod/browser_mod.js +++ b/custom_components/browser_mod/browser_mod.js @@ -1,4 +1,4 @@ -!function(e){var t={};function o(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,o),s.l=!0,s.exports}o.m=e,o.c=t,o.d=function(e,t,i){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(o.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)o.d(i,s,function(t){return e[t]}.bind(null,s));return i},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=1)}([function(e){e.exports=JSON.parse('{"name":"browser_mod","private":true,"version":"1.1.6","description":"","scripts":{"build":"webpack","watch":"webpack --watch --mode=development","update-card-tools":"npm uninstall card-tools && npm install thomasloven/lovelace-card-tools"},"keywords":[],"author":"Thomas Lovén","license":"MIT","devDependencies":{"webpack":"^4.44.1","webpack-cli":"^3.3.12"},"dependencies":{"card-tools":"github:thomasloven/lovelace-card-tools"}}')},function(e,t,o){"use strict";o.r(t);const i="lovelace-player-device-id";function s(){if(!localStorage[i]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);window.fully&&"function"==typeof fully.getDeviceId?localStorage[i]=fully.getDeviceId():localStorage[i]=`${e()}${e()}-${e()}${e()}`}return localStorage[i]}let a=s();const n=new URLSearchParams(window.location.search);var r;function l(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}function c(e){return document.querySelector("hc-main")?document.querySelector("hc-main").provideHass(e):document.querySelector("home-assistant")?document.querySelector("home-assistant").provideHass(e):void 0}function d(){var e,t=document.querySelector("hc-main");return t?((e=t._lovelaceConfig).current_view=t._lovelacePath,e):(t=(t=(t=(t=(t=(t=(t=(t=(t=document.querySelector("home-assistant"))&&t.shadowRoot)&&t.querySelector("home-assistant-main"))&&t.shadowRoot)&&t.querySelector("app-drawer-layout partial-panel-resolver"))&&t.shadowRoot||t)&&t.querySelector("ha-panel-lovelace"))&&t.shadowRoot)&&t.querySelector("hui-root"))?((e=t.lovelace).current_view=t.___curView,e):null}function u(){var e=document.querySelector("hc-main");return e=e?(e=(e=(e=e&&e.shadowRoot)&&e.querySelector("hc-lovelace"))&&e.shadowRoot)&&e.querySelector("hui-view")||e.querySelector("hui-panel-view"):(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=document.querySelector("home-assistant"))&&e.shadowRoot)&&e.querySelector("home-assistant-main"))&&e.shadowRoot)&&e.querySelector("app-drawer-layout partial-panel-resolver"))&&e.shadowRoot||e)&&e.querySelector("ha-panel-lovelace"))&&e.shadowRoot)&&e.querySelector("hui-root"))&&e.shadowRoot)&&e.querySelector("ha-app-layout"))&&e.querySelector("#view"))&&e.firstElementChild}async function h(){if(customElements.get("hui-view"))return!0;await customElements.whenDefined("partial-panel-resolver");const e=document.createElement("partial-panel-resolver");if(e.hass={panels:[{url_path:"tmp",component_name:"lovelace"}]},e._updateRoutes(),await e.routerOptions.routes.tmp.load(),!customElements.get("ha-panel-lovelace"))return!1;const t=document.createElement("ha-panel-lovelace");return t.hass=l(),void 0===t.hass&&(await new Promise(e=>{window.addEventListener("connection-status",t=>{console.log(t),e()},{once:!0})}),t.hass=l()),t.panel={config:{mode:null}},t._fetchConfig(),!0}async function m(e,t,o=!1){let i=e;"string"==typeof t&&(t=t.split(/(\$| )/));for(const[e,s]of t.entries())if(s.trim().length){if(!i)return null;i.localName&&i.localName.includes("-")&&await customElements.whenDefined(i.localName),i.updateComplete&&await i.updateComplete,i="$"===s?o&&e==t.length-1?[i.shadowRoot]:i.shadowRoot:o&&e==t.length-1?i.querySelectorAll(s):i.querySelector(s)}return i}async function p(e,t,o=!1,i=1e4){return Promise.race([m(e,t,o),new Promise((e,t)=>setTimeout(()=>t(new Error("timeout")),i))]).catch(e=>{if(!e.message||"timeout"!==e.message)throw e;return null})}function w(e,t,o=null){if((e=new Event(e,{bubbles:!0,cancelable:!1,composed:!0})).detail=t||{},o)o.dispatchEvent(e);else{var i=u();i&&i.dispatchEvent(e)}}n.get("deviceID")&&null!==(r=n.get("deviceID"))&&("clear"===r?localStorage.removeItem(i):localStorage[i]=r,a=s());let y=window.cardHelpers;new Promise(async(e,t)=>{y&&e();const o=async()=>{y=await window.loadCardHelpers(),window.cardHelpers=y,e()};window.loadCardHelpers?o():window.addEventListener("load",async()=>{h(),window.loadCardHelpers&&o()})});async function v(e,t,o=!1,i={},s=!1){if(!customElements.get("card-tools-popup")){const e=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),t=e.prototype.html,o=e.prototype.css;class i extends e{static get properties(){return{open:{},large:{reflect:!0,type:Boolean},hass:{}}}updated(e){e.has("hass")&&this.card&&(this.card.hass=this.hass)}closeDialog(){this.open=!1}async _makeCard(){const e=await window.loadCardHelpers();this.card=await e.createCardElement(this._card),this.card.hass=this.hass,this.requestUpdate()}async _applyStyles(){let e=await p(this,"$ ha-dialog");customElements.whenDefined("card-mod").then(async()=>{if(!e)return;customElements.get("card-mod").applyToElement(e,"more-info",this._style,{config:this._card},[],!1)})}async showDialog(e,t,o=!1,i={},s=!1){this.title=e,this._card=t,this.large=o,this._style=i,this.fullscreen=!!s,this._makeCard(),await this.updateComplete,this.open=!0,await this._applyStyles()}_enlarge(){this.large=!this.large}render(){return this.open?t` +!function(e){var t={};function o(s){if(t[s])return t[s].exports;var a=t[s]={i:s,l:!1,exports:{}};return e[s].call(a.exports,a,a.exports,o),a.l=!0,a.exports}o.m=e,o.c=t,o.d=function(e,t,s){o.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,t){if(1&t&&(e=o(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(o.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)o.d(s,a,function(t){return e[t]}.bind(null,a));return s},o.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(t,"a",t),t},o.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},o.p="",o(o.s=1)}([function(e){e.exports=JSON.parse('{"name":"browser_mod","private":true,"version":"1.2.0","description":"","scripts":{"build":"webpack","watch":"webpack --watch --mode=development","update-card-tools":"npm uninstall card-tools && npm install thomasloven/lovelace-card-tools"},"keywords":[],"author":"Thomas Lovén","license":"MIT","devDependencies":{"webpack":"^4.44.1","webpack-cli":"^3.3.12"},"dependencies":{"card-tools":"github:thomasloven/lovelace-card-tools"}}')},function(e,t,o){"use strict";o.r(t);const s="lovelace-player-device-id";function a(){if(!localStorage[s]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);window.fully&&"function"==typeof fully.getDeviceId?localStorage[s]=fully.getDeviceId():localStorage[s]=`${e()}${e()}-${e()}${e()}`}return localStorage[s]}let i=a();const n=new URLSearchParams(window.location.search);var r;function l(){return document.querySelector("hc-main")?document.querySelector("hc-main").hass:document.querySelector("home-assistant")?document.querySelector("home-assistant").hass:void 0}function c(e){return document.querySelector("hc-main")?document.querySelector("hc-main").provideHass(e):document.querySelector("home-assistant")?document.querySelector("home-assistant").provideHass(e):void 0}function d(){var e,t=document.querySelector("hc-main");return t?((e=t._lovelaceConfig).current_view=t._lovelacePath,e):(t=(t=(t=(t=(t=(t=(t=(t=(t=document.querySelector("home-assistant"))&&t.shadowRoot)&&t.querySelector("home-assistant-main"))&&t.shadowRoot)&&t.querySelector("app-drawer-layout partial-panel-resolver"))&&t.shadowRoot||t)&&t.querySelector("ha-panel-lovelace"))&&t.shadowRoot)&&t.querySelector("hui-root"))?((e=t.lovelace).current_view=t.___curView,e):null}function u(){var e=document.querySelector("hc-main");return e=e?(e=(e=(e=e&&e.shadowRoot)&&e.querySelector("hc-lovelace"))&&e.shadowRoot)&&e.querySelector("hui-view")||e.querySelector("hui-panel-view"):(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=(e=document.querySelector("home-assistant"))&&e.shadowRoot)&&e.querySelector("home-assistant-main"))&&e.shadowRoot)&&e.querySelector("app-drawer-layout partial-panel-resolver"))&&e.shadowRoot||e)&&e.querySelector("ha-panel-lovelace"))&&e.shadowRoot)&&e.querySelector("hui-root"))&&e.shadowRoot)&&e.querySelector("ha-app-layout"))&&e.querySelector("#view"))&&e.firstElementChild}async function h(){if(customElements.get("hui-view"))return!0;await customElements.whenDefined("partial-panel-resolver");const e=document.createElement("partial-panel-resolver");if(e.hass={panels:[{url_path:"tmp",component_name:"lovelace"}]},e._updateRoutes(),await e.routerOptions.routes.tmp.load(),!customElements.get("ha-panel-lovelace"))return!1;const t=document.createElement("ha-panel-lovelace");return t.hass=l(),void 0===t.hass&&(await new Promise(e=>{window.addEventListener("connection-status",t=>{console.log(t),e()},{once:!0})}),t.hass=l()),t.panel={config:{mode:null}},t._fetchConfig(),!0}async function p(e,t,o=!1){let s=e;"string"==typeof t&&(t=t.split(/(\$| )/)),""===t[t.length-1]&&t.pop();for(const[e,a]of t.entries())if(a.trim().length){if(!s)return null;s.localName&&s.localName.includes("-")&&await customElements.whenDefined(s.localName),s.updateComplete&&await s.updateComplete,s="$"===a?o&&e==t.length-1?[s.shadowRoot]:s.shadowRoot:o&&e==t.length-1?s.querySelectorAll(a):s.querySelector(a)}return s}async function m(e,t,o=!1,s=1e4){return Promise.race([p(e,t,o),new Promise((e,t)=>setTimeout(()=>t(new Error("timeout")),s))]).catch(e=>{if(!e.message||"timeout"!==e.message)throw e;return null})}function y(e,t,o=null){if((e=new Event(e,{bubbles:!0,cancelable:!1,composed:!0})).detail=t||{},o)o.dispatchEvent(e);else{var s=u();s&&s.dispatchEvent(e)}}n.get("deviceID")&&null!==(r=n.get("deviceID"))&&("clear"===r?localStorage.removeItem(s):localStorage[s]=r,i=a());let w=window.cardHelpers;new Promise(async(e,t)=>{w&&e();const o=async()=>{w=await window.loadCardHelpers(),window.cardHelpers=w,e()};window.loadCardHelpers?o():window.addEventListener("load",async()=>{h(),window.loadCardHelpers&&o()})});async function v(){const e=document.querySelector("home-assistant")||document.querySelector("hc-root");y("hass-more-info",{entityId:"."},e);const t=await m(e,"$ card-tools-popup");t&&t.closeDialog()}async function _(e,t,o=!1,s={},a=!1){if(!customElements.get("card-tools-popup")){const e=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),t=e.prototype.html,o=e.prototype.css;class s extends e{static get properties(){return{open:{},large:{reflect:!0,type:Boolean},hass:{}}}updated(e){e.has("hass")&&this.card&&(this.card.hass=this.hass)}closeDialog(){this.open=!1}async _makeCard(){const e=await window.loadCardHelpers();this.card=await e.createCardElement(this._card),this.card.hass=this.hass,this.requestUpdate()}async _applyStyles(){let e=await m(this,"$ ha-dialog");customElements.whenDefined("card-mod").then(async()=>{if(!e)return;customElements.get("card-mod").applyToElement(e,"more-info",this._style,{config:this._card},[],!1)})}async showDialog(e,t,o=!1,s={},a=!1){this.title=e,this._card=t,this.large=o,this._style=s,this.fullscreen=!!a,this._makeCard(),await this.updateComplete,this.open=!0,await this._applyStyles()}_enlarge(){this.large=!this.large}render(){return this.open?t` {if(e.state&&"cardToolsPopup"in e.state)if(e.state.cardToolsPopup){const{title:t,card:o,large:i,style:s,fullscreen:a}=e.state.params;v(t,o,i,s,a)}else n.closeDialog()};window.addEventListener("popstate",e),window._moreInfoDialogListener=!0}history.replaceState({cardToolsPopup:!1},""),history.pushState({cardToolsPopup:!0,params:{title:e,card:t,large:o,style:i,fullscreen:s}},""),n.showDialog(e,t,o,i,s)}async function g(e,t=!1){const o=document.querySelector("hc-main")||document.querySelector("home-assistant");w("hass-more-info",{entityId:e},o);const i=await p(o,"$ ha-more-info-dialog");return i&&(i.large=t),i}const f=[customElements.whenDefined("home-assistant-main"),customElements.whenDefined("hui-view")];Promise.race(f).then(()=>{const e=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),t=e.prototype.html;e.prototype.css;class o extends e{setConfig(e){}render(){return t` + `}}customElements.define("card-tools-popup",s)}const i=document.querySelector("home-assistant")||document.querySelector("hc-root");if(!i)return;let n=await m(i,"$ card-tools-popup");if(!n){n=document.createElement("card-tools-popup");const e=i.shadowRoot.querySelector("ha-more-info-dialog");e?i.shadowRoot.insertBefore(n,e):i.shadowRoot.appendChild(n),c(n)}if(!window._moreInfoDialogListener){const e=async e=>{if(e.state&&"cardToolsPopup"in e.state)if(e.state.cardToolsPopup){const{title:t,card:o,large:s,style:a,fullscreen:i}=e.state.params;_(t,o,s,a,i)}else n.closeDialog()};window.addEventListener("popstate",e),window._moreInfoDialogListener=!0}history.replaceState({cardToolsPopup:!1},""),history.pushState({cardToolsPopup:!0,params:{title:e,card:t,large:o,style:s,fullscreen:a}},""),n.showDialog(e,t,o,s,a)}function g(){if(!localStorage["lovelace-player-device-id"]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);window.fully&&"function"==typeof fully.getDeviceId?localStorage["lovelace-player-device-id"]=fully.getDeviceId():localStorage["lovelace-player-device-id"]=`${e()}${e()}-${e()}${e()}`}return localStorage["lovelace-player-device-id"]}let f=g();const b=e=>{null!==e&&("clear"===e?localStorage.removeItem("lovelace-player-device-id"):localStorage["lovelace-player-device-id"]=e,f=g())},S=new URLSearchParams(window.location.search);async function x(e,t=!1){const o=document.querySelector("hc-main")||document.querySelector("home-assistant");y("hass-more-info",{entityId:e},o);const s=await m(o,"$ ha-more-info-dialog");return s&&(s.large=t),s}S.get("deviceID")&&b(S.get("deviceID"));const E=[customElements.whenDefined("home-assistant-main"),customElements.whenDefined("hui-view")];Promise.race(E).then(()=>{const e=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),t=e.prototype.html;e.prototype.css;class o extends e{setConfig(e){}render(){return t`
Nothing to configure.
- `}}customElements.get("browser-player-editor")||(customElements.define("browser-player-editor",o),window.customCards=window.customCards||[],window.customCards.push({type:"browser-player",name:"Browser Player",preview:!0}))});const b=[customElements.whenDefined("home-assistant-main"),customElements.whenDefined("hui-view")];Promise.race(b).then(()=>{const e=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),t=e.prototype.html,o=e.prototype.css;customElements.get("browser-player")||customElements.define("browser-player",class extends e{static get properties(){return{hass:{}}}static getConfigElement(){return document.createElement("browser-player-editor")}static getStubConfig(){return{}}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){g("media_player."+window.browser_mod.entity_id)}handlePlayPause(e){window.browser_mod.player.paused?window.browser_mod.play({}):window.browser_mod.pause({})}render(){if(!window.browser_mod)return window.setTimeout(()=>this.requestUpdate(),100),t``;const e=window.browser_mod.player;return t` + `}}customElements.get("browser-player-editor")||(customElements.define("browser-player-editor",o),window.customCards=window.customCards||[],window.customCards.push({type:"browser-player",name:"Browser Player",preview:!0}))});const k=[customElements.whenDefined("home-assistant-main"),customElements.whenDefined("hui-view")];Promise.race(k).then(()=>{const e=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),t=e.prototype.html,o=e.prototype.css;customElements.get("browser-player")||customElements.define("browser-player",class extends e{static get properties(){return{hass:{}}}static getConfigElement(){return document.createElement("browser-player-editor")}static getStubConfig(){return{}}setConfig(e){this._config=e;for(const e of["play","pause","ended","volumechange","canplay","loadeddata"])window.browser_mod.player.addEventListener(e,()=>this.requestUpdate())}handleMute(e){window.browser_mod.player_mute()}handleVolumeChange(e){const t=parseFloat(e.target.value);window.browser_mod.player_set_volume(t)}handleMoreInfo(e){x("media_player."+window.browser_mod.entity_id)}handlePlayPause(e){window.browser_mod.player.paused?window.browser_mod.player_play():window.browser_mod.player_pause()}setDeviceID(){const e=prompt("Set deviceID",f);e!==f&&(b(e),this.requestUpdate())}render(){if(!window.browser_mod)return window.setTimeout(()=>this.requestUpdate(),100),t``;const e=window.browser_mod.player;return t`
- + > ${"stopped"===window.browser_mod.player_state?t`
`:t` - + > `} - + >
-
- ${a} +
+ ${f}
@@ -146,4 +146,4 @@ -moz-user-select: all; -ms-user-select: all; } - `}})});class S{set hass(e){if(!e)return;if(this._hass=e,this.hassPatched)return;const t=e.callService;e.callService=(e,o,i)=>{if(i&&i.deviceID)if(Array.isArray(i.deviceID)){const e=i.deviceID.indexOf("this");-1!==e&&((i=JSON.parse(JSON.stringify(i))).deviceID[e]=a)}else"this"===i.deviceID&&((i=JSON.parse(JSON.stringify(i))).deviceID=a);return t(e,o,i)},this.hassPatched=!0,document.querySelector("hc-main")?document.querySelector("hc-main").hassChanged(e,e):document.querySelector("home-assistant").hassChanged(e,e)}playOnce(e){this._video&&this._video.play(),window.browser_mod.playedOnce||(window.browser_mod.player.play(),window.browser_mod.playedOnce=!0)}async _load_lovelace(){if(!await h()){window.setTimeout(this._load_lovelace.bind(this),100)}}_connect(){window.hassConnection?window.hassConnection.then(e=>this.connect(e.conn)):window.setTimeout(()=>this._connect(),100)}constructor(){this.entity_id=a.replace("-","_"),this.cast=null!==document.querySelector("hc-main"),this.cast?this.connect(l().connection):(window.setTimeout(this._load_lovelace.bind(this),500),this._connect(),document.querySelector("home-assistant").addEventListener("hass-more-info",this.popup_card.bind(this))),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)),c(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);const t=o(0);console.info(`%cBROWSER_MOD ${t.version} IS INSTALLED\n %cDeviceID: ${a}`,"color: green; font-weight: bold","")}connect(e){this.conn=e,e.subscribeMessage(e=>this.callback(e),{type:"browser_mod/connect",deviceID:a})}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"toast":this.toast(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"window-reload":window.location.reload(!1);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"}popup_card(e){if(!d())return;const t=d(),o={...t.config.popup_cards,...t.config.views[t.current_view].popup_cards};if(!e.detail||!e.detail.entityId)return;const i=o[e.detail.entityId];i&&window.setTimeout(()=>{w("hass-more-info",{entityId:"."},document.querySelector("home-assistant")),v(i.title,i.card,i.large||!1,i.style)},50)}debug(e){v("deviceID",{type:"markdown",content:"# "+a}),alert(a)}_set_screensaver(e,t){if(clearTimeout(this._screenSaverTimer),e){if(-1==(t=parseInt(t)))return clearTimeout(this._screenSaverTimer),void(this._screenSaverTime=0);this._screenSaverTime=1e3*t,this._screenSaver=e,this._screenSaverTimer=setTimeout(this._screenSaver,this._screenSaverTime)}else this._screenSaverTime&&(this._screenSaverTimer=setTimeout(this._screenSaver,this._screenSaverTime))}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)}toast(e){e.message&&w("hass-notification",{message:e.message,duration:void 0!==e.duration?parseInt(e.duration):void 0},document.querySelector("home-assistant"))}popup(e){if(!e.title&&!e.auto_close)return;if(!e.card)return;const t=()=>{v(e.title,e.card,e.large,e.style,e.auto_close||e.hide_header),e.auto_close&&(this.autoclose_popup_active=!0)};e.auto_close&&e.time?this._set_screensaver(t,e.time):t()}close_popup(e){this._set_screensaver(),this.autoclose_popup_active=!1,async function(){const e=document.querySelector("home-assistant")||document.querySelector("hc-root");w("hass-more-info",{entityId:"."},e);const t=await p(e,"$ card-tools-popup");t&&t.closeDialog()}()}navigate(e){e.navigation_path&&(history.pushState(null,"",e.navigation_path),w("location-changed",{},document.querySelector("home-assistant")))}more_info(e){e.entity_id&&g(e.entity_id,e.large)}set_theme(e){e.theme||(e.theme="default"),w("settheme",e.theme,document.querySelector("home-assistant"))}lovelace_reload(e){const t=u();t&&w("config-refresh",{},t)}blackout(e){const t=()=>{window.fully?fully.turnScreenOff():this._blackout.style.visibility="visible",this.update()};e.time?this._set_screensaver(t,e.time):t()}no_blackout(e){if(this._set_screensaver(),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())}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()}start_camera(){this._video||(this._video=document.createElement("video"),this._video.autoplay=!0,this._video.playsInline=!0,this._video.style.cssText="\n visibility: hidden;\n width: 0;\n height: 0;\n ",this._canvas=document.createElement("canvas"),this._canvas.style.cssText="\n visibility: hidden;\n width: 0;\n height: 0;\n ",document.body.appendChild(this._canvas),document.body.appendChild(this._video),navigator.mediaDevices.getUserMedia({video:!0,audio:!1}).then(e=>{this._video.srcObject=e,this._video.play(),this.send_cam()}))}send_cam(e){this._canvas.getContext("2d").drawImage(this._video,0,0,this._canvas.width,this._canvas.height),this.conn.sendMessage({type:"browser_mod/update",deviceID:a,data:{camera:this._canvas.toDataURL("image/png")}}),setTimeout(this.send_cam.bind(this),5e3)}update(e=null){this.conn&&(e&&(e.name&&(this.entity_id=e.name.toLowerCase()),e.camera&&this.start_camera()),this.conn.sendMessage({type:"browser_mod/update",deviceID:a,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}}))}}const k=[customElements.whenDefined("home-assistant-main"),customElements.whenDefined("hui-view")];Promise.race(k).then(()=>{window.browser_mod=window.browser_mod||new S})}]); + `}})});class q{async connect(){if(null!==document.querySelector("hc-main"))this._connection=l().connection;else{if(!window.hassConnection)return void window.setTimeout(()=>this._do_connect(),100);this._connection=(await window.hassConnection).conn}this._connection.subscribeMessage(e=>this.msg_callback(e),{type:"browser_mod/connect",deviceID:f}),this._hass_patched=!1,c(this)}set hass(e){if(this._hass=e,!e||this._hass_patched)return;this._hass_patched=!0;const t=e.callService;e.callService=(e,o,s)=>{if(s&&s.deviceID){s=JSON.parse(JSON.stringify(s));const e=JSON.stringify(s.deviceID).replace('"this"',`"${f}"`);s.deviceID=JSON.parse(e)}return t(e,o,s)},document.querySelector("hc-main")?document.querySelector("hc-main").hassChanged(e,e):document.querySelector("home-assistant").hassChanged(e,e)}get connected(){return void 0!==this._connection}msg_callback(e){console.log(e)}sendUpdate(e){this.connected&&this._connection.sendMessage({type:"browser_mod/update",deviceID:f,data:e})}}const D=e=>class extends e{constructor(){super(),this.player=new Audio;for(const e of["play","pause","ended","volumechange"])this.player.addEventListener(e,()=>this.player_update());window.addEventListener("click",()=>this.player.play(),{once:!0})}player_update(e){this.sendUpdate({player:{volume:this.player.volume,muted:this.player.muted,src:this.player.src,state:this.player_state}})}get player_state(){return this.player.src?this.player.ended?"stopped":this.player.paused?"paused":"playing":"stopped"}player_play(e){e&&(this.player.src=e),this.player.play()}player_pause(){this.player.pause()}player_stop(){this.player.pause(),this.player.src=null}player_set_volume(e){void 0!==e&&(this.player.volume=e)}player_mute(e){void 0===e&&(e=!this.player.muted),this.player.muted=Boolean(e)}},O=e=>class extends e{get isFully(){return void 0!==window.fully}constructor(){if(super(),this.isFully){this._fullyMotion=!1,this._motionTimeout=void 0;for(const e of["screenOn","screenOff","pluggedAC","pluggedUSB","onBatteryLevelChanged","unplugged","networkReconnect"])fully.bind(e,"window.browser_mod.fully_update();");window.fully.bind("onMotion","window.browser_mod.fullyMotionTriggered();")}}fully_update(){this.isFully&&this.sendUpdate({fully:{battery:window.fully.getBatteryLevel(),charging:window.fully.isPlugged(),motion:this._fullyMotion}})}fullyMotionTriggered(){this._fullyMotion=!0,clearTimeout(this._motionTimeout),this._motionTimeout=setTimeout(()=>{this._fullyMotion=!1,this.fully_update()},5e3),this.fully_update()}},$=e=>class extends e{setup_camera(){console.log("Starting camera"),this._video||(this._video=document.createElement("video"),this._video.autoplay=!0,this._video.playsInline=!0,this._video.style.display="none",this._canvas=document.createElement("canvas"),this._canvas.style.display="none",document.body.appendChild(this._video),document.body.appendChild(this._canvas),navigator.mediaDevices&&(console.log("Starting devices"),navigator.mediaDevices.getUserMedia({video:!0,audio:!1}).then(e=>{this._video.srcObject=e,this._video.play(),this.update_camera()}),this._camera_framerate=2,window.addEventListener("click",()=>this._video.play(),{once:!0})))}update_camera(){this._canvas.width=this._video.videoWidth,this._canvas.height=this._video.videoHeight;this._canvas.getContext("2d").drawImage(this._video,0,0,this._video.videoWidth,this._video.videoHeight),this.sendUpdate({camera:this._canvas.toDataURL("image/jpeg")}),setTimeout(()=>this.update_camera(),Math.round(1e3/this._camera_framerate))}},I=e=>class extends e{constructor(){super(),this._blackout_panel=document.createElement("div"),this._screenSaver=void 0,this._screenSaverTimer=void 0,this._screenSaverTimeOut=0,this._screenSaver={fn:void 0,clearfn:void 0,timer:void 0,timeout:void 0,listeners:{},active:!1},this._blackout_panel.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 display: none;\n ",document.body.appendChild(this._blackout_panel),this.isFully&&(window.fully.bind("screenOn","window.browser_mod.screen_update();"),window.fully.bind("screenOff","window.browser_mod.screen_update();"))}screensaver_set(e,t,o){this._ss_clear(),this._screenSaver={fn:e,clearfn:t,timer:void 0,timeout:o,listeners:{},active:!1};const s=()=>this.screensaver_update();for(const e of["mousemove","mousedown","keydown","touchstart"])window.addEventListener(e,s),this._screenSaver.listeners[e]=s;this._screenSaver.timer=window.setTimeout(()=>this._ss_run(),1e3*o)}screensaver_update(){this._screenSaver.active?this.screensaver_stop():(window.clearTimeout(this._screenSaver.timer),this._screenSaver.timer=window.setTimeout(()=>this._ss_run(),1e3*this._screenSaver.timeout))}screensaver_stop(){this._ss_clear(),this._screenSaver.active=!1,this._screenSaver.clearfn&&this._screenSaver.clearfn(),this._screenSaver.timeout&&this.screensaver_set(this._screenSaver.fn,this._screenSaver.clearfn,this._screenSaver.timeout)}_ss_clear(){window.clearTimeout(this._screenSaverTimer);for(const[e,t]of Object.entries(this._screenSaver.listeners))window.removeEventListener(e,t)}_ss_run(){this._screenSaver.active=!0,this._screenSaver.fn()}do_blackout(e){this.screensaver_set(()=>{this.isFully?window.fully.turnScreenOff():this._blackout_panel.style.display="block",this.screen_update()},()=>{(this._blackout_panel.style.display="block")&&(this._blackout_panel.style.display="none"),this.isFully&&!window.fully.getScreenOn()&&window.fully.turnScreenOn(),this.screen_update()},e||0)}no_blackout(){this.screensaver_stop()}screen_update(){this.sendUpdate({screen:{blackout:this.isFully?!window.fully.getScreenOn():Boolean("block"===this._blackout_panel.style.display),brightness:this.isFully?window.fully.getScreenBrightness():void 0}})}},C=e=>class extends e{constructor(){super(),document.querySelector("home-assistant")&&document.querySelector("home-assistant").addEventListener("hass-more-info",e=>this._popup_card(e));null!==document.querySelector("hc-main")||h()}_popup_card(e){if(!d())return;if(!e.detail||!e.detail.entityId)return;const t={...d().config.popup_cards,...d().config.views[d().current_view].popup_cards}[e.detail.entityId];t&&window.queueMicrotask(()=>{y("hass-more-info",{entityID:"."},document.querySelector("home-assistant")),_(t.title,t.card,t.large||!1,t.style)})}do_popup(e){if(!(e.title||e.auto_close||e.hide_header))return;if(!e.card)return;const t=()=>{_(e.tile,e.card,e.large,e.style,e.auto_close||e.hide_header)};e.auto_close?this.screensaver_set(t,v,e.time):t()}do_close_popup(){this.screensaver_stop(),v()}do_more_info(e,t){e&&x(e,t)}do_toast(e,t){e&&y("hass-notification",{message:e,duration:parseInt(t)},document.querySelector("home-assistant"))}},P=e=>class extends e{constructor(){super(),document.addEventListener("visibilitychange",()=>this.sensor_update()),window.addEventListener("location-changed",()=>this.sensor_update()),window.setInterval(()=>this.sensor_update(),1e4)}sensor_update(){window.queueMicrotask(async()=>{const e=navigator.getBattery?await navigator.getBattery():void 0;this.sendUpdate({browser:{path:window.location.pathname,visibility:document.visibilityState,userAgent:navigator.userAgent,currentUser:this._hass&&this._hass.user&&this._hass.user.name,fullyKiosk:this.isFully,width:window.innerWidth,height:window.innerHeight,battery_level:this.isFully?window.fully.getBatteryLevel():e?100*e.level:void 0,charging:this.isFully?window.fully.isPlugged():e?e.charging:void 0}})})}do_navigate(e){e&&(history.pushState(null,"",e),y("location-changed",{},document.querySelector("home-assistant")))}};class T extends(((e,t)=>t.reduceRight((e,t)=>t(e),e))(q,[P,C,I,$,O,D])){constructor(){super(),this.entity_id=i.replace("-","_"),this.cast=null!==document.querySelector("hc-main"),this.connect();const e=o(0);console.info(`%cBROWSER_MOD ${e.version} IS INSTALLED\n %cDeviceID: ${i}`,"color: green; font-weight: bold","")}msg_callback(e){({update:e=>this.update(e),debug:e=>this.debug(e),play:e=>this.player_play(e.media_content_id),pause:e=>this.player_pause(),stop:e=>this.player_stop(),set_volume:e=>this.player_set_volume(e.volume_level),mute:e=>this.player_mute(e.mute),toast:e=>this.do_toast(e.message,e.duration),popup:e=>this.do_popup(e),"close-popup":e=>this.do_close_popup(),"more-info":e=>this.do_more_info(e.entity_id,e.large),navigate:e=>this.do_navigate(e.navigation_path),"set-theme":e=>this.set_theme(e),"lovelace-reload":e=>this.lovelace_reload(e),"window-reload":()=>window.location.reload(!1),blackout:e=>this.do_blackout(e.time?parseInt(e.time):void 0),"no-blackout":e=>{e.brightness&&this.isFully&&window.fully.setScreenBrightness(e.brightness),this.no_blackout()}})[e.command](e)}debug(e){_("deviceID",{type:"markdown",content:"# "+i}),alert(i)}set_theme(e){e.theme||(e.theme="default"),y("settheme",{theme:e.theme},document.querySelector("home-assistant"))}lovelace_reload(e){const t=u();t&&y("config-refresh",{},t)}update(e=null){e&&(e.name&&(this.entity_id=e.name.toLowerCase()),e.camera&&this.setup_camera()),this.player_update(),this.fully_update(),this.screen_update(),this.sensor_update()}}const M=[customElements.whenDefined("home-assistant"),customElements.whenDefined("hc-main")];Promise.race(M).then(()=>{window.browser_mod=window.browser_mod||new T})}]); \ No newline at end of file diff --git a/custom_components/browser_mod/camera.py b/custom_components/browser_mod/camera.py index 497fc89..59baad4 100644 --- a/custom_components/browser_mod/camera.py +++ b/custom_components/browser_mod/camera.py @@ -21,7 +21,8 @@ class BrowserModCamera(Camera, BrowserModEntity): self.last_seen = None def updated(self): - self.last_seen = datetime.now() + 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): diff --git a/custom_components/browser_mod/connection.py b/custom_components/browser_mod/connection.py index e56ab79..964381b 100644 --- a/custom_components/browser_mod/connection.py +++ b/custom_components/browser_mod/connection.py @@ -1,14 +1,19 @@ import logging import voluptuous as vol -from homeassistant.components.websocket_api import websocket_command, result_message, event_message, async_register_command -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.components.websocket_api import ( + websocket_command, + result_message, + event_message, + async_register_command +) -from .const import DOMAIN, WS_CONNECT, WS_UPDATE, WS_CAMERA -from .helpers import get_devices, create_entity, get_config +from .const import WS_CONNECT, WS_UPDATE +from .helpers import get_devices, create_entity, get_config, is_setup_complete _LOGGER = logging.getLogger(__name__) + async def setup_connection(hass, config): @websocket_command({ @@ -18,7 +23,8 @@ async def setup_connection(hass, config): def handle_connect(hass, connection, msg): deviceID = msg["deviceID"] - device = get_devices(hass).get(deviceID, BrowserModConnection(hass, deviceID)) + device = get_devices(hass).get(deviceID, + BrowserModConnection(hass, deviceID)) device.connect(connection, msg["id"]) get_devices(hass)[deviceID] = device @@ -29,7 +35,7 @@ async def setup_connection(hass, config): vol.Required("deviceID"): str, vol.Optional("data"): dict, }) - def handle_update( hass, connection, msg): + def handle_update(hass, connection, msg): devices = get_devices(hass) deviceID = msg["deviceID"] if deviceID in devices: @@ -38,6 +44,7 @@ async def setup_connection(hass, config): async_register_command(hass, handle_connect) async_register_command(hass, handle_update) + class BrowserModConnection: def __init__(self, hass, deviceID): self.hass = hass @@ -52,7 +59,7 @@ class BrowserModConnection: def connect(self, connection, cid): self.connection.append((connection, cid)) - self.send("update", **get_config(self.hass, self.deviceID)) + self.trigger_update() def disconnect(): self.connection.remove((connection, cid)) @@ -67,6 +74,10 @@ class BrowserModConnection: **kwargs, })) + def trigger_update(self): + if is_setup_complete(self.hass): + self.send("update", **get_config(self.hass, self.deviceID)) + def update(self, data): if data.get('browser'): self.sensor = self.sensor or create_entity( @@ -112,4 +123,3 @@ class BrowserModConnection: self) if self.camera: self.camera.data = data.get('camera') - diff --git a/custom_components/browser_mod/const.py b/custom_components/browser_mod/const.py index e3fc012..95a54d6 100644 --- a/custom_components/browser_mod/const.py +++ b/custom_components/browser_mod/const.py @@ -8,6 +8,7 @@ DATA_DEVICES = "devices" DATA_ALIASES = "aliases" DATA_ADDERS = "adders" DATA_CONFIG = "config" +DATA_SETUP_COMPLETE = "setup_complete" CONFIG_DEVICES = "devices" CONFIG_PREFIX = "prefix" diff --git a/custom_components/browser_mod/helpers.py b/custom_components/browser_mod/helpers.py index f533ac3..b65b5fb 100644 --- a/custom_components/browser_mod/helpers.py +++ b/custom_components/browser_mod/helpers.py @@ -2,35 +2,53 @@ import logging from homeassistant.helpers.entity import Entity, async_generate_entity_id -from .const import DOMAIN, DATA_DEVICES, DATA_ALIASES, DATA_ADDERS, CONFIG_DEVICES, DATA_CONFIG, CONFIG_PREFIX, CONFIG_DISABLE, CONFIG_DISABLE_ALL +from .const import ( + DOMAIN, + DATA_DEVICES, + DATA_ALIASES, + DATA_ADDERS, + CONFIG_DEVICES, + DATA_CONFIG, + CONFIG_PREFIX, + CONFIG_DISABLE, + CONFIG_DISABLE_ALL, + DATA_SETUP_COMPLETE, +) _LOGGER = logging.getLogger(__name__) + def get_devices(hass): return hass.data[DOMAIN][DATA_DEVICES] + def get_alias(hass, deviceID): - for k,v in hass.data[DOMAIN][DATA_ALIASES].items(): + for k, v in hass.data[DOMAIN][DATA_ALIASES].items(): if v == deviceID: return k return None + def get_config(hass, deviceID): config = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DEVICES, {}) - return config.get(deviceID, config.get(deviceID.replace('-','_'), {})) + return config.get(deviceID, config.get(deviceID.replace('-', '_'), {})) + def create_entity(hass, platform, deviceID, connection): conf = get_config(hass, deviceID) if conf and (platform in conf.get(CONFIG_DISABLE, []) - or CONFIG_DISABLE_ALL in conf.get(CONFIG_DISABLE, [])): + or CONFIG_DISABLE_ALL in conf.get(CONFIG_DISABLE, [])): return None - if not conf and (platform in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, []) - or CONFIG_DISABLE_ALL in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, [])): + if not conf and \ + (platform in hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, []) + or CONFIG_DISABLE_ALL in + hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_DISABLE, [])): return None adder = hass.data[DOMAIN][DATA_ADDERS][platform] entity = adder(hass, deviceID, connection, get_alias(hass, deviceID)) return entity + def setup_platform(hass, config, async_add_devices, platform, cls): def adder(hass, deviceID, connection, alias=None): entity = cls(hass, connection, deviceID, alias) @@ -39,6 +57,11 @@ def setup_platform(hass, config, async_add_devices, platform, cls): hass.data[DOMAIN][DATA_ADDERS][platform] = adder return True + +def is_setup_complete(hass): + return hass.data[DOMAIN][DATA_SETUP_COMPLETE] + + class BrowserModEntity(Entity): def __init__(self, hass, connection, deviceID, alias=None): @@ -47,7 +70,11 @@ class BrowserModEntity(Entity): self.deviceID = deviceID self._data = {} prefix = hass.data[DOMAIN][DATA_CONFIG].get(CONFIG_PREFIX, '') - self.entity_id = async_generate_entity_id(self.domain+".{}", alias or f"{prefix}{deviceID}", hass=hass) + self.entity_id = async_generate_entity_id( + self.domain+".{}", + alias or f"{prefix}{deviceID}", + hass=hass + ) def updated(self): pass @@ -55,6 +82,7 @@ class BrowserModEntity(Entity): @property def data(self): return self._data + @data.setter def data(self, data): self._data = data diff --git a/custom_components/browser_mod/light.py b/custom_components/browser_mod/light.py index 77fd342..ffd1fb8 100644 --- a/custom_components/browser_mod/light.py +++ b/custom_components/browser_mod/light.py @@ -48,6 +48,10 @@ class BrowserModLight(LightEntity, BrowserModEntity): return SUPPORT_BRIGHTNESS return 0 + @property + def brightness(self): + return self.data.get('brightness', None) + def turn_on(self, **kwargs): self.connection.send("no-blackout", **kwargs) diff --git a/custom_components/browser_mod/services.yaml b/custom_components/browser_mod/services.yaml index a1c58ba..f9225fe 100644 --- a/custom_components/browser_mod/services.yaml +++ b/custom_components/browser_mod/services.yaml @@ -1,9 +1,85 @@ command: - description: Send a command to a browser + description: 'Send a command to a browser.' fields: command: - description: Command to send + description: 'Command to send' example: 'navigate' deviceID: - description: List of receiving browsers + description: 'List of receiving browsers' example: '["99980b13-dabc9563", "office_computer"]' +debug: + description: 'On all browsers, show a popup, and a javascript alert with the current device ID.' +set_theme: + description: 'On all browsers, change the theme.' + fields: + theme: + description: 'Theme to change to' + example: '{theme: "clear_light"}' +navigate: + description: 'Navigate to a path on a browser.' + fields: + navigation_path: + description: 'Path to navigate to' + example: '/lovelace/1' + deviceID: + description: 'List of receiving browsers' + example: '["99980b13-dabc9563", "office_computer"]' +more_info: + description: 'Open the more info dialog of an entity on a browser.' + fields: + entity_id: + description: 'Entity to show more info for' + example: 'camera.front_door' + deviceID: + description: 'List of receiving browsers' + example: '["99980b13-dabc9563", "office_computer"]' + large: + description: '(optional) Set to true to make wider' + example: 'true' +toast: + description: 'Show a toast message in the bottom left on all browsers.' + fields: + message: + description: 'Message to show' + example: 'Short message' + duration: + description: '(optional) Time in milliseconds to show message for. Set to 0 for persistent display.' + example: '10000' +popup: + description: 'Pop up a card on a browser.' + fields: + title: + description: 'Name to show in popup bar' + example: 'Popup example' + card: + description: 'YAML config for card to show' + deviceID: + description: 'List of receiving browsers' + example: '["99980b13-dabc9563", "office_computer"]' + large: + description: '(optional) Set to true to make wider' + example: 'true' + hide_header: + description: '(optional) Hide header title and close button' + example: 'true' + auto_close: + description: '(optional) Close popup when mouse is moved or key is pressed. Also hides header' + example: 'true' + time: + description: "(optional) When mouse isn't moved or keys aren't pressed for this amount of seconds, reopen. Only usable with auto_close. See blackout" + example: '20' +close_popup: + description: 'Close all popups on all browsers.' +blackout: + description: 'Cover screen in black until the mouse is moved or a key is pressed.' + fields: + time: + description: '(optional) The blackout will turn on automatically after the specified number of seconds. It works kind of like a screensaver and will keep turning on until blackout is called again with time: -1.' + example: '20' +no_blackout: + description: 'Remove a blackout from a browser.' + fields: + brightness: + description: '(optional) On a Fully Kiosk Browser Plus set the screen brightness from 0 - 255.' +lovelace_reload: + description: 'Refresh the lovelace configuration.' diff --git a/js/browser-player.js b/js/browser-player.js index 11a0f72..b0fa460 100644 --- a/js/browser-player.js +++ b/js/browser-player.js @@ -1,5 +1,4 @@ -import { LitElement, html, css } from "card-tools/src/lit-element"; -import { deviceID } from "card-tools/src/deviceID" +import { deviceID, setDeviceID } from "card-tools/src/deviceId" import { moreInfo } from "card-tools/src/more-info" import "./browser-player-editor.js" @@ -29,22 +28,31 @@ class BrowserPlayer extends LitElement { setConfig(config) { this._config = config; + for (const event of ["play", "pause", "ended", "volumechange", "canplay", "loadeddata"]) + window.browser_mod.player.addEventListener(event, () => this.requestUpdate()); } handleMute(ev) { - window.browser_mod.mute({}); + window.browser_mod.player_mute(); } handleVolumeChange(ev) { const vol = parseFloat(ev.target.value); - window.browser_mod.set_volume({volume_level: vol}); + window.browser_mod.player_set_volume(vol); } handleMoreInfo(ev) { moreInfo("media_player."+window.browser_mod.entity_id); } handlePlayPause(ev) { if (window.browser_mod.player.paused) - window.browser_mod.play({}); + window.browser_mod.player_play(); else - window.browser_mod.pause({}); + window.browser_mod.player_pause(); + } + setDeviceID() { + const newID = prompt("Set deviceID", deviceID); + if (newID !== deviceID) { + setDeviceID(newID); + this.requestUpdate(); + } } render() { @@ -63,34 +71,34 @@ class BrowserPlayer extends LitElement { } @click=${this.handleMute} > - + > ${window.browser_mod.player_state === "stopped" ? html`
` : html` - + > `} - + >
-
+
${deviceID}
diff --git a/js/browser.js b/js/browser.js new file mode 100644 index 0000000..09b7b92 --- /dev/null +++ b/js/browser.js @@ -0,0 +1,35 @@ +import { fireEvent } from "card-tools/src/event"; + +export const BrowserModBrowserMixin = (C) => class extends C { + + constructor() { + super(); + document.addEventListener("visibilitychange", () => this.sensor_update()); + window.addEventListener("location-changed", () => this.sensor_update()); + + window.setInterval(() => this.sensor_update(), 10000); + } + + sensor_update() { + window.queueMicrotask( async () => { + const battery = navigator.getBattery ? await navigator.getBattery() : undefined; + this.sendUpdate({browser: { + path: window.location.pathname, + visibility: document.visibilityState, + userAgent: navigator.userAgent, + currentUser: this._hass &&this._hass.user && this._hass.user.name, + fullyKiosk: this.isFully, + width: window.innerWidth, + height: window.innerHeight, + battery_level: this.isFully ? window.fully.getBatteryLevel() : battery ? battery.level*100 : undefined, + charging: this.isFully ? window.fully.isPlugged() : battery ? battery.charging : undefined, + }}); + }); + } + + do_navigate(path) { + if (!path) return; + history.pushState(null, "", path); + fireEvent("location-changed", {}, document.querySelector("home-assistant")); + } +} diff --git a/js/camera.js b/js/camera.js new file mode 100644 index 0000000..1212c89 --- /dev/null +++ b/js/camera.js @@ -0,0 +1,46 @@ +export const BrowserModCameraMixin = (C) => class extends C { + + setup_camera() { + + console.log("Starting camera") + + if(this._video) return; + this._video = document.createElement("video"); + this._video.autoplay = true; + this._video.playsInline = true; + this._video.style.display = "none"; + + this._canvas = document.createElement("canvas"); + this._canvas.style.display = "none"; + + document.body.appendChild(this._video); + document.body.appendChild(this._canvas); + + if(!navigator.mediaDevices) return; + + console.log("Starting devices") + navigator.mediaDevices.getUserMedia({video: true, audio: false}).then((stream) => { + this._video.srcObject = stream; + this._video.play(); + this.update_camera(); + }) + + this._camera_framerate = 2; + + window.addEventListener("click", () => this._video.play(), {once: true}); + } + + update_camera() { + this._canvas.width = this._video.videoWidth; + this._canvas.height = this._video.videoHeight; + + const context = this._canvas.getContext('2d'); + context.drawImage(this._video, 0, 0, this._video.videoWidth, this._video.videoHeight); + + this.sendUpdate({ + camera: this._canvas.toDataURL('image/jpeg'), + }); + setTimeout(() => this.update_camera(), Math.round(1000 / this._camera_framerate)); + } + +} diff --git a/js/connection.js b/js/connection.js new file mode 100644 index 0000000..9ae7621 --- /dev/null +++ b/js/connection.js @@ -0,0 +1,71 @@ +import { deviceID } from "card-tools/src/deviceId"; +import { hass, provideHass } from "card-tools/src/hass"; + +export class BrowserModConnection{ + + async connect() { + const isCast = document.querySelector("hc-main") !== null; + if(!isCast) { + if(!window.hassConnection) { + window.setTimeout(() => this._do_connect(), 100); + return; + } else { + this._connection = (await window.hassConnection).conn; + } + } else { + this._connection = hass().connection; + } + + this._connection.subscribeMessage((msg) => this.msg_callback(msg), { + type: 'browser_mod/connect', + deviceID: deviceID, + }); + + this._hass_patched = false; + provideHass(this); + } + + set hass(hass) { + this._hass = hass; + if(!hass || this._hass_patched) return; + + this._hass_patched = true; + const callService = hass.callService; + + hass.callService = (domain, service, serviceData) => { + if(serviceData && serviceData.deviceID) { + serviceData = JSON.parse(JSON.stringify(serviceData)); + + const orig = JSON.stringify(serviceData.deviceID); + const patched = orig.replace('"this"', `"${deviceID}"`); + serviceData.deviceID = JSON.parse(patched); + } + return callService(domain, service, serviceData); + } + + if (document.querySelector("hc-main")) + document.querySelector("hc-main").hassChanged(hass, hass); + else + document.querySelector("home-assistant").hassChanged(hass, hass); + } + + get connected() { + return this._connection !== undefined; + } + + msg_callback(message) { + console.log(message); + } + + sendUpdate(data) { + if(!this.connected) return; + this._connection.sendMessage({ + type: 'browser_mod/update', + deviceID, + data, + } + ) + } + + +} diff --git a/js/fullyKiosk.js b/js/fullyKiosk.js new file mode 100644 index 0000000..1d4aaf0 --- /dev/null +++ b/js/fullyKiosk.js @@ -0,0 +1,39 @@ +export const FullyKioskMixin = (C) => class extends C { + get isFully() { + return window.fully !== undefined; + } + + constructor() { + super(); + + if (!this.isFully) return; + + this._fullyMotion = false; + this._motionTimeout = undefined; + + for (const event of ["screenOn", "screenOff", "pluggedAC", "pluggedUSB", "onBatteryLevelChanged", "unplugged", "networkReconnect"]) { + fully.bind(event, "window.browser_mod.fully_update();"); + } + + window.fully.bind("onMotion", "window.browser_mod.fullyMotionTriggered();"); + } + + fully_update() { + if(!this.isFully) return + this.sendUpdate({fully: { + battery: window.fully.getBatteryLevel(), + charging: window.fully.isPlugged(), + motion: this._fullyMotion, + }}) + } + + fullyMotionTriggered() { + this._fullyMotion = true; + clearTimeout(this._motionTimeout); + this._motionTimeout = setTimeout(() => { + this._fullyMotion = false; + this.fully_update(); + }, 5000); + this.fully_update(); + } +} diff --git a/js/main.js b/js/main.js index 5a69243..8dff9f8 100644 --- a/js/main.js +++ b/js/main.js @@ -1,123 +1,36 @@ import { deviceID } from "card-tools/src/deviceID"; -import { lovelace_view, provideHass, load_lovelace, lovelace, hass } from "card-tools/src/hass"; -import { popUp, closePopUp } from "card-tools/src/popup"; +import { lovelace_view } from "card-tools/src/hass"; +import { popUp } from "card-tools/src/popup"; import { fireEvent } from "card-tools/src/event"; -import { moreInfo } from "card-tools/src/more-info.js"; import "./browser-player"; -class BrowserMod { +import { BrowserModConnection } from "./connection"; +import { BrowserModMediaPlayerMixin } from "./mediaPlayer"; +import { FullyKioskMixin } from "./fullyKiosk"; +import { BrowserModCameraMixin } from "./camera"; +import { BrowserModScreensaverMixin } from "./screensaver"; +import { BrowserModPopupsMixin } from "./popups"; +import { BrowserModBrowserMixin } from "./browser"; - set hass(hass) { - if(!hass) return; - this._hass = hass; - if(this.hassPatched) return; - const callService = hass.callService; - const newCallService = (domain, service, serviceData) => { - if(serviceData && serviceData.deviceID) { - if(Array.isArray(serviceData.deviceID)) { - const index = serviceData.deviceID.indexOf('this'); - if(index !== -1) { - serviceData = JSON.parse(JSON.stringify(serviceData)); - serviceData.deviceID[index] = deviceID; - } - } else if(serviceData.deviceID === "this") { - serviceData = JSON.parse(JSON.stringify(serviceData)); - serviceData.deviceID = deviceID; - } - } - return callService(domain, service, serviceData); - }; - hass.callService = newCallService; - this.hassPatched = true; - if(document.querySelector("hc-main")) - document.querySelector("hc-main").hassChanged(hass,hass); - else - document.querySelector("home-assistant").hassChanged(hass, hass); - } +const ext = (baseClass, mixins) => + mixins.reduceRight((base, mixin) => mixin(base), baseClass); - playOnce(ev) { - if(this._video) this._video.play(); - if(window.browser_mod.playedOnce) return; - window.browser_mod.player.play(); - window.browser_mod.playedOnce = true; - } +class BrowserMod extends ext(BrowserModConnection, [ + BrowserModBrowserMixin, + BrowserModPopupsMixin, + BrowserModScreensaverMixin, + BrowserModCameraMixin, + FullyKioskMixin, + BrowserModMediaPlayerMixin, + ]) { - async _load_lovelace() { - if(!await load_lovelace()) { - let timer = window.setTimeout(this._load_lovelace.bind(this), 100); - } - } - - _connect() { - if(!window.hassConnection) { - window.setTimeout(() => this._connect(), 100); - } else { - window.hassConnection.then((conn) => this.connect(conn.conn)); - } - } constructor() { - this.entity_id = deviceID.replace("-","_"); + super(); + this.entity_id = deviceID.replace("-", "_"); this.cast = document.querySelector("hc-main") !== null; - if(!this.cast) { - window.setTimeout(this._load_lovelace.bind(this), 500); - this._connect(); - document.querySelector("home-assistant").addEventListener("hass-more-info", this.popup_card.bind(this)); - } else { - this.connect(hass().connection); - } - - this.player = new Audio(); - this.playedOnce = false; - - this.autoclose_popup_active = false; - - const updater = this.update.bind(this); - this.player.addEventListener("ended", updater); - this.player.addEventListener("play", updater); - this.player.addEventListener("pause", updater); - this.player.addEventListener("volumechange", updater); - document.addEventListener("visibilitychange", updater); - window.addEventListener("location-changed", updater); - window.addEventListener("click", this.playOnce); - window.addEventListener("mousemove", this.no_blackout.bind(this)); - window.addEventListener("mousedown", this.no_blackout.bind(this)); - window.addEventListener("keydown", this.no_blackout.bind(this)); - window.addEventListener("touchstart", this.no_blackout.bind(this)); - provideHass(this); - - if(window.fully) - { - this._fullyMotion = false; - this._motionTimeout = undefined; - fully.bind('screenOn', 'browser_mod.update();'); - fully.bind('screenOff', 'browser_mod.update();'); - fully.bind('pluggedAC', 'browser_mod.update();'); - fully.bind('pluggedUSB', 'browser_mod.update();'); - fully.bind('onBatteryLevelChanged', 'browser_mod.update();'); - fully.bind('unplugged', 'browser_mod.update();'); - fully.bind('networkReconnect', 'browser_mod.update();'); - - fully.bind('onMotion', 'browser_mod.fullyMotion();'); - } - - this._screenSaver = undefined; - this._screenSaverTimer = undefined; - this._screenSaverTime = 0; - this._blackout = document.createElement("div"); - this._blackout.style.cssText = ` - position: fixed; - left: 0; - top: 0; - padding: 0; - margin: 0; - width: 100%; - height: 100%; - background: black; - visibility: hidden; - `; - document.body.appendChild(this._blackout); + this.connect(); const pjson = require('../package.json'); console.info(`%cBROWSER_MOD ${pjson.version} IS INSTALLED @@ -125,96 +38,37 @@ class BrowserMod { "color: green; font-weight: bold", ""); } - connect(conn) { - this.conn = conn - conn.subscribeMessage((msg) => this.callback(msg), { - type: 'browser_mod/connect', - deviceID: deviceID, - }); - } + msg_callback(msg) { + const handlers = { + update: (msg) => this.update(msg), + debug: (msg) => this.debug(msg), - callback(msg) { - switch (msg.command) { - case "update": - this.update(msg); - break; + play: (msg) => this.player_play(msg.media_content_id), + pause: (msg) => this.player_pause(), + stop: (msg) => this.player_stop(), + set_volume: (msg) => this.player_set_volume(msg.volume_level), + mute: (msg) => this.player_mute(msg.mute), - case "debug": - this.debug(msg); - break; + toast: (msg) => this.do_toast(msg.message, msg.duration), + popup: (msg) => this.do_popup(msg), + "close-popup": (msg) => this.do_close_popup(), + "more-info": (msg) => this.do_more_info(msg.entity_id, msg.large), - case "play": - this.play(msg); - break; - case "pause": - this.pause(msg); - break; - case "stop": - this.stop(msg); - break; - case "set_volume": - this.set_volume(msg); - break; - case "mute": - this.mute(msg); - break; + navigate: (msg) => this.do_navigate(msg.navigation_path), + "set-theme": (msg) => this.set_theme(msg), + "lovelace-reload": (msg) => this.lovelace_reload(msg), + "window-reload": () => window.location.reload(false), - case "toast": - this.toast(msg); - break; - case "popup": - this.popup(msg); - break; - case "close-popup": - this.close_popup(msg); - break; - case "navigate": - this.navigate(msg); - break; - case "more-info": - this.more_info(msg); - break; - case "set-theme": - this.set_theme(msg); - break; - case "lovelace-reload": - this.lovelace_reload(msg); - break; - case "window-reload": - window.location.reload(false); - break; - - case "blackout": - this.blackout(msg); - break; - case "no-blackout": - this.no_blackout(msg); - break; - } - } - - get player_state() { - if (!this.player.src) return "stopped"; - if (this.player.ended) return "stopped"; - if (this.player.paused) return "paused"; - return "playing"; - } - - popup_card(ev) { - if(!lovelace()) return; - const ll = lovelace(); - const data = { - ...ll.config.popup_cards, - ...ll.config.views[ll.current_view].popup_cards, + blackout: (msg) => this.do_blackout(msg.time ? parseInt(msg.time) : undefined), + "no-blackout": (msg) => { + if(msg.brightness && this.isFully) { + window.fully.setScreenBrightness(msg.brightness); + } + this.no_blackout() + }, }; - if(!ev.detail || !ev.detail.entityId) return; - const d = data[ev.detail.entityId]; - if(!d) return; - window.setTimeout(() => { - fireEvent("hass-more-info", {entityId: "."}, document.querySelector("home-assistant")); - popUp(d.title, d.card, d.large || false, d.style); - }, 50); + handlers[msg.command](msg); } debug(msg) { @@ -222,90 +76,9 @@ class BrowserMod { alert(deviceID); } - _set_screensaver(fn, time) { - clearTimeout(this._screenSaverTimer); - if(!fn) { - if(this._screenSaverTime) - this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime) - } else { - time = parseInt(time) - if(time == -1) { - clearTimeout(this._screenSaverTimer); - this._screenSaverTime = 0; - return; - } - this._screenSaverTime = time * 1000; - this._screenSaver = fn; - this._screenSaverTimer = setTimeout(this._screenSaver, this._screenSaverTime) - } - } - - play(msg) { - const src = msg.media_content_id; - if(src) - this.player.src = src; - this.player.play(); - } - pause(msg) { - this.player.pause(); - } - stop(msg) { - this.player.pause(); - this.player.src = null; - } - set_volume(msg) { - if (msg.volume_level === undefined) return; - this.player.volume = msg.volume_level; - } - mute(msg) { - if (msg.mute === undefined) - msg.mute = !this.player.muted; - this.player.muted = Boolean(msg.mute) - } - - toast(msg) { - if(!msg.message) return; - - fireEvent("hass-notification", { - message: msg.message, - duration: msg.duration !== undefined ? parseInt(msg.duration) : undefined - }, document.querySelector("home-assistant")); - } - - popup(msg){ - if(!msg.title && !msg.auto_close) return; - if(!msg.card) return; - - const fn = () => { - popUp(msg.title, msg.card, msg.large, msg.style, msg.auto_close||msg.hide_header); - if(msg.auto_close) - this.autoclose_popup_active = true; - }; - - if(msg.auto_close && msg.time) { - this._set_screensaver(fn, msg.time); - } else { - // closePopUp(); - fn(); - } - } - close_popup(msg){ - this._set_screensaver(); - this.autoclose_popup_active = false; - closePopUp(); - } - navigate(msg){ - if(!msg.navigation_path) return; - history.pushState(null, "", msg.navigation_path); - fireEvent("location-changed", {}, document.querySelector("home-assistant")); - } - more_info(msg){ - if(!msg.entity_id) return; - moreInfo(msg.entity_id, msg.large); - } set_theme(msg){ if(!msg.theme) msg.theme = "default"; - fireEvent("settheme", msg.theme, document.querySelector("home-assistant")); + fireEvent("settheme", {theme: msg.theme}, document.querySelector("home-assistant")); } lovelace_reload(msg) { @@ -314,145 +87,25 @@ class BrowserMod { fireEvent("config-refresh", {}, ll); } - blackout(msg){ - const fn = () => { - if (window.fully) - { - fully.turnScreenOff(); - } else { - this._blackout.style.visibility = "visible"; - } - this.update(); - }; - if(msg.time) { - this._set_screensaver(fn, msg.time) - } else { - fn(); - } - } - no_blackout(msg){ - this._set_screensaver(); - if(this.autoclose_popup_active) - return this.close_popup(); - if (window.fully) - { - if (!fully.getScreenOn()) - fully.turnScreenOn(); - if (msg.brightness) - fully.setScreenBrightness(msg.brightness); - this.update(); - } else { - if(this._blackout.style.visibility !== "hidden") { - this._blackout.style.visibility = "hidden"; - this.update(); - } - } - } - is_blackout(){ - if (window.fully) - return !fully.getScreenOn(); - return Boolean(this._blackout.style.visibility === "visible") - } - - fullyMotion() { - this._fullyMotion = true; - clearTimeout(this._motionTimeout); - this._motionTimeout = setTimeout(() => { - this._fullyMotion = false; - this.update(); - }, 5000); - this.update(); - } - - - start_camera() { - if(this._video) return; - this._video = document.createElement("video"); - this._video.autoplay = true; - this._video.playsInline = true; - this._video.style.cssText = ` - visibility: hidden; - width: 0; - height: 0; - `; - this._canvas = document.createElement("canvas"); - this._canvas.style.cssText = ` - visibility: hidden; - width: 0; - height: 0; - `; - document.body.appendChild(this._canvas); - document.body.appendChild(this._video); - navigator.mediaDevices.getUserMedia({video: true, audio: false}).then((stream) => { - this._video.srcObject = stream; - this._video.play(); - this.send_cam(); - }); - } - - send_cam(data) { - const context = this._canvas.getContext('2d'); - context.drawImage(this._video, 0, 0, this._canvas.width, this._canvas.height); - this.conn.sendMessage({ - type: 'browser_mod/update', - deviceID: deviceID, - data: { - camera: this._canvas.toDataURL('image/png'), - }, - }); - setTimeout(this.send_cam.bind(this), 5000); - } - - update(msg=null) { - if(!this.conn) return; - if(msg) { if(msg.name) { this.entity_id = msg.name.toLowerCase(); } if(msg.camera) { - this.start_camera(); + this.setup_camera(); } } - - this.conn.sendMessage({ - type: 'browser_mod/update', - deviceID: deviceID, - data: { - browser: { - path: window.location.pathname, - visibility: document.visibilityState, - userAgent: navigator.userAgent, - currentUser: this._hass && this._hass.user && this._hass.user.name, - fullyKiosk: window.fully ? true : undefined, - width: window.innerWidth, - height: window.innerHeight, - }, - player: { - volume: this.player.volume, - muted: this.player.muted, - src: this.player.src, - state: this.player_state, - }, - screen: { - blackout: this.is_blackout(), - brightness: window.fully ? fully.getScreenBrightness() : undefined, - }, - fully: window.fully ? { - battery: window.fully ? fully.getBatteryLevel() : undefined, - charging: window.fully ? fully.isPlugged(): undefined, - motion: window.fully ? this._fullyMotion : undefined, - } : undefined, - }, - }); - + this.player_update(); + this.fully_update(); + this.screen_update(); + this.sensor_update(); } } -const bases = [customElements.whenDefined('home-assistant-main'), customElements.whenDefined('hui-view')]; +const bases = [customElements.whenDefined('home-assistant'), customElements.whenDefined('hc-main')]; Promise.race(bases).then(() => { window.browser_mod = window.browser_mod || new BrowserMod(); }); diff --git a/js/mediaPlayer.js b/js/mediaPlayer.js new file mode 100644 index 0000000..c2fd804 --- /dev/null +++ b/js/mediaPlayer.js @@ -0,0 +1,51 @@ +export const BrowserModMediaPlayerMixin = (C) => class extends C { + + constructor() { + super(); + this.player = new Audio(); + + for (const event of ["play", "pause", "ended", "volumechange"]) { + this.player.addEventListener(event, () => this.player_update()); + } + + window.addEventListener("click", () => this.player.play(), {once: true}); + } + + player_update(ev) { + this.sendUpdate({player: { + volume: this.player.volume, + muted: this.player.muted, + src: this.player.src, + state: this.player_state, + }}) + } + + get player_state() { + if (!this.player.src) return "stopped"; + if (this.player.ended) return "stopped"; + if (this.player.paused) return "paused"; + return "playing"; + } + + player_play(src) { + if(src) + this.player.src = src; + this.player.play(); + } + player_pause() { + this.player.pause(); + } + player_stop() { + this.player.pause(); + this.player.src = null; + } + player_set_volume(level) { + if(level === undefined) return; + this.player.volume = level; + } + player_mute(mute) { + if(mute === undefined) + mute = !this.player.muted; + this.player.muted = Boolean(mute); + } +} diff --git a/js/popups.js b/js/popups.js new file mode 100644 index 0000000..96f86f8 --- /dev/null +++ b/js/popups.js @@ -0,0 +1,79 @@ +import { fireEvent } from "card-tools/src/event"; +import { load_lovelace, lovelace } from "card-tools/src/hass"; +import { moreInfo } from "card-tools/src/more-info"; +import { closePopUp, popUp } from "card-tools/src/popup"; + +export const BrowserModPopupsMixin = (C) => class extends C { + + constructor() { + super(); + if (document.querySelector("home-assistant")) + document.querySelector("home-assistant").addEventListener("hass-more-info", (ev) => this._popup_card(ev)); + + const isCast = document.querySelector("hc-main") !== null; + if(!isCast) + load_lovelace(); + } + + _popup_card(ev) { + if(!lovelace()) return; + if(!ev.detail || !ev.detail.entityId) return; + const data = { + ...lovelace().config.popup_cards, + ...lovelace().config.views[lovelace().current_view].popup_cards, + }; + const d = data[ev.detail.entityId]; + if(!d) return; + + window.queueMicrotask(() => { + fireEvent("hass-more-info", {entityID: "."}, document.querySelector("home-assistant")); + popUp( + d.title, + d.card, + d.large || false, + d.style + ); + }); + } + + do_popup(cfg) { + if (!(cfg.title || cfg.auto_close || cfg.hide_header)) return; + if (!cfg.card) return; + + const open = () => { + popUp( + cfg.tile, + cfg.card, + cfg.large, + cfg.style, + cfg.auto_close || cfg.hide_header, + ); + }; + + if(cfg.auto_close) { + this.screensaver_set(open, closePopUp, cfg.time); + } else { + open(); + } + } + + do_close_popup() { + this.screensaver_stop(); + closePopUp(); + } + + do_more_info(entity_id, large) { + if (!entity_id) return; + moreInfo(entity_id, large); + } + + do_toast(message, duration) { + if (!message) return; + fireEvent("hass-notification", { + message, + duration: parseInt(duration), + }, document.querySelector("home-assistant")); + } + + +} diff --git a/js/screensaver.js b/js/screensaver.js new file mode 100644 index 0000000..8aa9c5a --- /dev/null +++ b/js/screensaver.js @@ -0,0 +1,123 @@ +export const BrowserModScreensaverMixin = (C) => class extends C { + constructor() { + super(); + this._blackout_panel = document.createElement("div"); + + this._screenSaver = undefined; + this._screenSaverTimer = undefined; + this._screenSaverTimeOut = 0; + + this._screenSaver = { + fn: undefined, + clearfn: undefined, + timer: undefined, + timeout: undefined, + listeners : {}, + active: false, + }; + + this._blackout_panel.style.cssText = ` + position: fixed; + left: 0; + top: 0; + padding: 0; + margin: 0; + width: 100%; + height: 100%; + background: black; + display: none; + `; + document.body.appendChild(this._blackout_panel); + + if(this.isFully) { + window.fully.bind("screenOn", "window.browser_mod.screen_update();"); + window.fully.bind("screenOff", "window.browser_mod.screen_update();"); + } + } + + screensaver_set(fn, clearfn, time) { + this._ss_clear(); + this._screenSaver = { + fn, + clearfn, + timer: undefined, + timeout: time, + listeners: {}, + active: false, + } + const l = () => this.screensaver_update(); + for(const event of ["mousemove", "mousedown", "keydown", "touchstart"]) { + window.addEventListener(event, l); + this._screenSaver.listeners[event] = l; + } + this._screenSaver.timer = window.setTimeout(() => this._ss_run(), time*1000); + } + + screensaver_update() { + if (this._screenSaver.active) { + this.screensaver_stop(); + } else { + window.clearTimeout(this._screenSaver.timer); + this._screenSaver.timer = window.setTimeout(() => this._ss_run(), this._screenSaver.timeout*1000); + } + } + + screensaver_stop() { + this._ss_clear(); + this._screenSaver.active = false; + if(this._screenSaver.clearfn) + this._screenSaver.clearfn(); + if(this._screenSaver.timeout) { + this.screensaver_set( + this._screenSaver.fn, + this._screenSaver.clearfn, + this._screenSaver.timeout, + ); + } + } + + _ss_clear() { + window.clearTimeout(this._screenSaverTimer); + for(const [k, v] of Object.entries(this._screenSaver.listeners)) { + window.removeEventListener(k, v); + } + } + + _ss_run() { + this._screenSaver.active = true; + this._screenSaver.fn(); + } + + do_blackout(timeout) { + this.screensaver_set( + () => { + if(this.isFully) + window.fully.turnScreenOff(); + else + this._blackout_panel.style.display = "block"; + this.screen_update(); + }, + () => { + if(this._blackout_panel.style.display = "block") + this._blackout_panel.style.display = "none" + if(this.isFully && !window.fully.getScreenOn()) + window.fully.turnScreenOn(); + this.screen_update(); + }, + timeout || 0 + ); + } + + no_blackout() { + this.screensaver_stop(); + } + + screen_update() { + this.sendUpdate({screen: { + blackout: this.isFully + ? !window.fully.getScreenOn() + : Boolean(this._blackout_panel.style.display === "block"), + brightness: this.isFully ? window.fully.getScreenBrightness() : undefined, + }}) + } +} diff --git a/package-lock.json b/package-lock.json index 00d5bf4..3b8118c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "browser_mod", - "version": "1.1.6", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -656,7 +656,7 @@ "dev": true }, "card-tools": { - "version": "github:thomasloven/lovelace-card-tools#6d5ae3800e4937aa424edc17108f43b964aecce7", + "version": "github:thomasloven/lovelace-card-tools#1272cf67c56e8f576e24c13f510568d544ad5d0b", "from": "github:thomasloven/lovelace-card-tools" }, "chalk": { diff --git a/package.json b/package.json index 17d331b..4e4220e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "browser_mod", "private": true, - "version": "1.1.6", + "version": "1.2.0", "description": "", "scripts": { "build": "webpack", diff --git a/test/configuration.yaml b/test/configuration.yaml index f47cadc..a4b67ba 100644 --- a/test/configuration.yaml +++ b/test/configuration.yaml @@ -3,6 +3,12 @@ default_config: demo: browser_mod: + devices: + camdevice: + camera: true + testdevice: + alias: test + lovelace: mode: yaml @@ -17,6 +23,8 @@ lovelace: frontend: themes: + red: + primary-color: red test: card-mod-theme: test card-mod-more-info-yaml: | @@ -24,3 +32,12 @@ frontend: .mdc-dialog { backdrop-filter: grayscale(0.7) blur(5px); } + +tts: + - platform: google_translate + base_url: !env_var OUT_ADDR + +script: + cm_debug: + sequence: + - service: browser_mod.debug diff --git a/test/docker-compose.yml b/test/docker-compose.yml index ab87356..1250760 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -52,6 +52,8 @@ services: volumes: *x-files ports: - "5001:8123" + environment: + OUT_ADDR: "http://${DOCKER_GATEWAY_HOST:-localhost}:5001" command: *x-command dev: