From 5458ee065435b2e3ab6108ea95c33afa29081384 Mon Sep 17 00:00:00 2001 From: Mathias Scherer Date: Sun, 4 Apr 2021 22:26:13 +0200 Subject: [PATCH] adds support for covers remove HA event listeners on shutdown --- MMM-HomeAssistant-Touch.css | 20 ++++++++- MMM-HomeAssistant-Touch.js | 1 + UIClasses/Base.js | 72 ++++++++++++++++--------------- UIClasses/Cover.js | 85 +++++++++++++++++++++++++++++++++++++ UIClasses/Light.js | 6 +-- UIClasses/Switch.js | 8 ++-- helpers/UIClassFactory.js | 2 + node_helper.js | 56 +++++++++++++++++------- 8 files changed, 190 insertions(+), 60 deletions(-) create mode 100644 UIClasses/Cover.js diff --git a/MMM-HomeAssistant-Touch.css b/MMM-HomeAssistant-Touch.css index 02b937a..828db1c 100644 --- a/MMM-HomeAssistant-Touch.css +++ b/MMM-HomeAssistant-Touch.css @@ -17,9 +17,25 @@ #MMM-HomeAssistant-Touch .ha-entity.ha-switch.on, #MMM-HomeAssistant-Touch .ha-entity.ha-light.on { - background-color: #2AF20261; + background-color: #2af20261; +} + +.ha-slider { + width: 3rem; + height: 200px; + border: 0.1rem solid grey; + position: absolute; +} + +.ha-slider .ha-slider-fill { + background-color: #2af20261; + width: 3rem; + position: absolute; + bottom: 0; + left: 0; + text-align: center; } body { cursor: default; -} +} \ No newline at end of file diff --git a/MMM-HomeAssistant-Touch.js b/MMM-HomeAssistant-Touch.js index d9a6110..14d0a7d 100644 --- a/MMM-HomeAssistant-Touch.js +++ b/MMM-HomeAssistant-Touch.js @@ -20,6 +20,7 @@ Module.register("MMM-HomeAssistant-Touch", { this.file("./UIClasses/Base.js"), this.file("./UIClasses/Light.js"), this.file("./UIClasses/Switch.js"), + this.file("./UIClasses/Cover.js"), this.file("./UIClasses/Unsupported.js"), ]; }, diff --git a/UIClasses/Base.js b/UIClasses/Base.js index 6ece783..0bba2d7 100644 --- a/UIClasses/Base.js +++ b/UIClasses/Base.js @@ -1,37 +1,39 @@ -class Base { - constructor(id, mm) { - this.id = id; - this.type = id.split('.')[0] - this.name = id; - this.mm = mm; - } - - updateState(state) { - this.name = (state.attributes || {}).friendly_name || this.id; - this.state = state.state; - this.render(); - } - - getContainer() { - const entity = document.createElement("div"); - entity.classList.add("ha-entity"); - entity.classList.add(`ha-${this.type}`) - entity.id = this.id; - entity.innerHTML = "Loading..."; - return entity; - } - - render() { - const container = document.getElementById(this.id); - container.className = "" - container.classList.add("ha-entity"); - container.classList.add(`ha-${this.type}`) +class Base { + constructor(id, mm) { + this.id = id; + this.type = id.split(".")[0]; + this.name = id; + this.mm = mm; + } - const title = document.createElement("span"); - title.className = "title"; - title.innerHTML = this.name; + updateState(state) { + this.name = (state.attributes || {}).friendly_name || this.id; + this.state = state.state; + this.render(); + } - container.innerHTML = ""; - container.appendChild(title); - } -} \ No newline at end of file + getContainer() { + const entity = document.createElement("div"); + entity.classList.add("ha-entity"); + entity.classList.add(`ha-${this.type}`); + entity.id = this.id; + entity.innerHTML = "Loading..."; + return entity; + } + + render() { + const container = document.getElementById(this.id); + if (container) { + container.className = ""; + container.classList.add("ha-entity"); + container.classList.add(`ha-${this.type}`); + + const title = document.createElement("span"); + title.className = "title"; + title.innerHTML = this.name; + + container.innerHTML = ""; + container.appendChild(title); + } + } +} diff --git a/UIClasses/Cover.js b/UIClasses/Cover.js new file mode 100644 index 0000000..35b8f3e --- /dev/null +++ b/UIClasses/Cover.js @@ -0,0 +1,85 @@ +class Cover extends Base { + constructor(...params) { + super(...params); + this.onSliderMove = this.onSliderMove.bind(this); + this.removeSlider = this.removeSlider.bind(this); + } + + getContainer() { + const entity = super.getContainer(); + entity.onmousedown = (event) => { + this.addSlider(event.x, event.y); + }; + entity.onmouseup = () => { + this.removeSlider(); + }; + return entity; + } + + updateState(state) { + this.name = (state.attributes || {}).friendly_name || this.id; + this.state = state.attributes.current_position; + this.render(); + } + + render() { + super.render(); + const container = document.getElementById(this.id); + if (container) { + container.classList.add(this.state); + } + } + + addSlider(offsetX, offsetY) { + const slider = document.createElement("div"); + slider.id = `slider-${this.id}`; + slider.classList.add("ha-slider"); + + // click location minus height minus margins + slider.style.top = `calc(${offsetY}px - 60px - 200px + calc(2px * ${this.state}))`; + // click location minus width/2 minus margins + slider.style.left = `calc(${offsetX}px - 60px - 1.5rem)`; + + const sliderFill = document.createElement("div"); + sliderFill.id = `slider-fill-${this.id}`; + sliderFill.classList.add("ha-slider-fill"); + sliderFill.style.height = `${this.state}%`; + sliderFill.innerHTML = this.state; + + slider.appendChild(sliderFill); + document.body.appendChild(slider); + + document.body.addEventListener("mouseup", this.removeSlider); + document.body.addEventListener("mousemove", this.onSliderMove); + + this.sliderStartY = offsetY; + this.sliderState = this.state; + } + + onSliderMove(event) { + const sliderFill = document.getElementById(`slider-fill-${this.id}`); + if (sliderFill) { + const offset = this.sliderStartY - event.y; + this.sliderState = this.state + Math.round((100 / 200) * offset); + sliderFill.style.height = `${this.sliderState}%`; + sliderFill.innerHTML = this.sliderState; + } + } + + removeSlider() { + const slider = document.getElementById(`slider-${this.id}`); + if (slider) { + slider.remove(); + } + document.body.removeEventListener("mouseup", this.removeSlider); + document.body.removeEventListener("mousemove", this.onSliderMove); + this.sendNewState(); + } + + sendNewState() { + this.mm.sendSocketNotification("SET_COVER_POSITION", { + entity: this.id, + position: this.sliderState, + }); + } +} diff --git a/UIClasses/Light.js b/UIClasses/Light.js index 55ef3b8..f2fcf02 100644 --- a/UIClasses/Light.js +++ b/UIClasses/Light.js @@ -14,8 +14,8 @@ class Light extends Base { render() { super.render(); const container = document.getElementById(this.id); - container.classList.add(this.state); - - container.appendChild(statusCheckbox); + if (container) { + container.classList.add(this.state); + } } } diff --git a/UIClasses/Switch.js b/UIClasses/Switch.js index 51a7a23..a01c178 100644 --- a/UIClasses/Switch.js +++ b/UIClasses/Switch.js @@ -6,7 +6,7 @@ class Switch extends Base { }; entity.ontouchend = () => { this.mm.sendSocketNotification("TOGGLE_STATE", { entity: this.id }); - } + }; return entity; } @@ -14,8 +14,8 @@ class Switch extends Base { render() { super.render(); const container = document.getElementById(this.id); - container.classList.add(this.state) - - container.appendChild(statusCheckbox); + if (container) { + container.classList.add(this.state); + } } } diff --git a/helpers/UIClassFactory.js b/helpers/UIClassFactory.js index 38a18ae..0e9545b 100644 --- a/helpers/UIClassFactory.js +++ b/helpers/UIClassFactory.js @@ -7,6 +7,8 @@ class UIClassFactory { return Light; case "switch": return Switch; + case "cover": + return Cover; default: return Unsupported; } diff --git a/node_helper.js b/node_helper.js index d7d78c8..b33bb71 100644 --- a/node_helper.js +++ b/node_helper.js @@ -3,24 +3,36 @@ const HomeAssistant = require("homeassistant"); const HomeAssistantWS = require("homeassistant-ws"); const Logger = require("./helpers/Logger"); -const connections = {}; - module.exports = NodeHelper.create({ start, + stop, socketNotificationReceived, connect, getState, toggleState, + setCoverPosition, onStateChangedEvent, }); function start() { this.logger = new Logger(this.name); + this.connections = {}; +} + +function stop() { + for (const connection in this.connections) { + this.connections[connection].websocket.unsubscribeFromEvent( + "state_changed" + ); + } } function socketNotificationReceived(notification, payload) { this.logger.debug(`Recieved notification ${notification}`, payload); - if (notification !== 'CONNECT' && (!payload.identifier || !connections[payload.identifier])) { + if ( + notification !== "CONNECT" && + (!payload.identifier || !this.connections[payload.identifier]) + ) { this.logger.error(`No connection for ${payload.identifier} found`); return; } @@ -35,6 +47,9 @@ function socketNotificationReceived(notification, payload) { case "TOGGLE_STATE": this.toggleState(payload); break; + case "SET_COVER_POSITION": + this.setCoverPosition(payload); + break; } } @@ -47,7 +62,7 @@ async function connect(payload) { }; const hass = new HomeAssistant(connectionConfig); this.logger.info(`HomeAssistant connected for ${payload.identifier}`); - connections[payload.identifier] = { + this.connections[payload.identifier] = { hass, entities: [], }; @@ -58,8 +73,8 @@ async function connect(payload) { host: new URL(connectionConfig.host).host, }) .then((hassWs) => { - connections[payload.identifier].websocket = hassWs; - hassWs.onStateChanged(onStateChangedEvent.bind(self)); + this.connections[payload.identifier].websocket = hassWs; + hassWs.onEvent("state_changed", onStateChangedEvent.bind(self)); }) .catch((err) => { this.logger.error( @@ -70,8 +85,8 @@ async function connect(payload) { } async function getState(payload) { - this.logger.debug(`Getting state for ${payload.entity}`); - const hass = connections[payload.identifier].hass; + this.logger.debug(`Getting state for ${payload.entity}`); + const hass = this.connections[payload.identifier].hass; const [domain, entity] = payload.entity.split("."); const response = await hass.states.get(domain, entity); this.logger.debug(`Got state for ${payload.entity}`); @@ -80,24 +95,33 @@ async function getState(payload) { data: response, }); - if (!connections[payload.identifier].entities.includes(payload.entity)) { - connections[payload.identifier].entities.push(payload.entity); + if (!this.connections[payload.identifier].entities.includes(payload.entity)) { + this.connections[payload.identifier].entities.push(payload.entity); } } async function toggleState(payload) { this.logger.debug(`Toggling state for ${payload.entity}`); - const hass = connections[payload.identifier].hass; + const hass = this.connections[payload.identifier].hass; const [domain, entity] = payload.entity.split("."); - const response = await hass.services.call('toggle', domain, entity) - this.logger.debug(`Response for toggling state of ${payload.entity}`, response) - this.getState(payload) + const response = await hass.services.call("toggle", domain, entity); + this.getState(payload); +} + +async function setCoverPosition(payload) { + this.logger.debug(`Setting position for cover ${payload.entity} to ${payload.position}`) + const hass = this.connections[payload.identifier].hass; + const response = await hass.services.call("set_cover_position", 'cover', { + entity_id: payload.entity, + position: payload.position + }); + this.getState(payload); } function onStateChangedEvent(event) { //this.logger.debug(`Got state change for ${event.data.entity_id}`); - for (const connection in connections) { - if (connections[connection].entities.includes(event.data.entity_id)) { + for (const connection in this.connections) { + if (this.connections[connection].entities.includes(event.data.entity_id)) { this.logger.debug( `Found listening connection (${connection}) for entity ${event.data.entity_id}` );