adds support for covers

remove HA event listeners on shutdown
This commit is contained in:
Mathias Scherer 2021-04-04 22:26:13 +02:00
parent 95fd422f78
commit 5458ee0654
No known key found for this signature in database
GPG Key ID: 6A618EAA2BC2C1E5
8 changed files with 190 additions and 60 deletions

View File

@ -17,7 +17,23 @@
#MMM-HomeAssistant-Touch .ha-entity.ha-switch.on, #MMM-HomeAssistant-Touch .ha-entity.ha-switch.on,
#MMM-HomeAssistant-Touch .ha-entity.ha-light.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 { body {

View File

@ -20,6 +20,7 @@ Module.register("MMM-HomeAssistant-Touch", {
this.file("./UIClasses/Base.js"), this.file("./UIClasses/Base.js"),
this.file("./UIClasses/Light.js"), this.file("./UIClasses/Light.js"),
this.file("./UIClasses/Switch.js"), this.file("./UIClasses/Switch.js"),
this.file("./UIClasses/Cover.js"),
this.file("./UIClasses/Unsupported.js"), this.file("./UIClasses/Unsupported.js"),
]; ];
}, },

View File

@ -1,37 +1,39 @@
class Base { class Base {
constructor(id, mm) { constructor(id, mm) {
this.id = id; this.id = id;
this.type = id.split('.')[0] this.type = id.split(".")[0];
this.name = id; this.name = id;
this.mm = mm; this.mm = mm;
} }
updateState(state) { updateState(state) {
this.name = (state.attributes || {}).friendly_name || this.id; this.name = (state.attributes || {}).friendly_name || this.id;
this.state = state.state; this.state = state.state;
this.render(); this.render();
} }
getContainer() { getContainer() {
const entity = document.createElement("div"); const entity = document.createElement("div");
entity.classList.add("ha-entity"); entity.classList.add("ha-entity");
entity.classList.add(`ha-${this.type}`) entity.classList.add(`ha-${this.type}`);
entity.id = this.id; entity.id = this.id;
entity.innerHTML = "Loading..."; entity.innerHTML = "Loading...";
return entity; return entity;
} }
render() { render() {
const container = document.getElementById(this.id); const container = document.getElementById(this.id);
container.className = "" if (container) {
container.classList.add("ha-entity"); container.className = "";
container.classList.add(`ha-${this.type}`) container.classList.add("ha-entity");
container.classList.add(`ha-${this.type}`);
const title = document.createElement("span"); const title = document.createElement("span");
title.className = "title"; title.className = "title";
title.innerHTML = this.name; title.innerHTML = this.name;
container.innerHTML = ""; container.innerHTML = "";
container.appendChild(title); container.appendChild(title);
} }
}
} }

85
UIClasses/Cover.js Normal file
View File

@ -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,
});
}
}

View File

@ -14,8 +14,8 @@ class Light extends Base {
render() { render() {
super.render(); super.render();
const container = document.getElementById(this.id); const container = document.getElementById(this.id);
container.classList.add(this.state); if (container) {
container.classList.add(this.state);
container.appendChild(statusCheckbox); }
} }
} }

View File

@ -6,7 +6,7 @@ class Switch extends Base {
}; };
entity.ontouchend = () => { entity.ontouchend = () => {
this.mm.sendSocketNotification("TOGGLE_STATE", { entity: this.id }); this.mm.sendSocketNotification("TOGGLE_STATE", { entity: this.id });
} };
return entity; return entity;
} }
@ -14,8 +14,8 @@ class Switch extends Base {
render() { render() {
super.render(); super.render();
const container = document.getElementById(this.id); const container = document.getElementById(this.id);
container.classList.add(this.state) if (container) {
container.classList.add(this.state);
container.appendChild(statusCheckbox); }
} }
} }

View File

@ -7,6 +7,8 @@ class UIClassFactory {
return Light; return Light;
case "switch": case "switch":
return Switch; return Switch;
case "cover":
return Cover;
default: default:
return Unsupported; return Unsupported;
} }

View File

@ -3,24 +3,36 @@ const HomeAssistant = require("homeassistant");
const HomeAssistantWS = require("homeassistant-ws"); const HomeAssistantWS = require("homeassistant-ws");
const Logger = require("./helpers/Logger"); const Logger = require("./helpers/Logger");
const connections = {};
module.exports = NodeHelper.create({ module.exports = NodeHelper.create({
start, start,
stop,
socketNotificationReceived, socketNotificationReceived,
connect, connect,
getState, getState,
toggleState, toggleState,
setCoverPosition,
onStateChangedEvent, onStateChangedEvent,
}); });
function start() { function start() {
this.logger = new Logger(this.name); 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) { function socketNotificationReceived(notification, payload) {
this.logger.debug(`Recieved notification ${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`); this.logger.error(`No connection for ${payload.identifier} found`);
return; return;
} }
@ -35,6 +47,9 @@ function socketNotificationReceived(notification, payload) {
case "TOGGLE_STATE": case "TOGGLE_STATE":
this.toggleState(payload); this.toggleState(payload);
break; break;
case "SET_COVER_POSITION":
this.setCoverPosition(payload);
break;
} }
} }
@ -47,7 +62,7 @@ async function connect(payload) {
}; };
const hass = new HomeAssistant(connectionConfig); const hass = new HomeAssistant(connectionConfig);
this.logger.info(`HomeAssistant connected for ${payload.identifier}`); this.logger.info(`HomeAssistant connected for ${payload.identifier}`);
connections[payload.identifier] = { this.connections[payload.identifier] = {
hass, hass,
entities: [], entities: [],
}; };
@ -58,8 +73,8 @@ async function connect(payload) {
host: new URL(connectionConfig.host).host, host: new URL(connectionConfig.host).host,
}) })
.then((hassWs) => { .then((hassWs) => {
connections[payload.identifier].websocket = hassWs; this.connections[payload.identifier].websocket = hassWs;
hassWs.onStateChanged(onStateChangedEvent.bind(self)); hassWs.onEvent("state_changed", onStateChangedEvent.bind(self));
}) })
.catch((err) => { .catch((err) => {
this.logger.error( this.logger.error(
@ -70,8 +85,8 @@ async function connect(payload) {
} }
async function getState(payload) { async function getState(payload) {
this.logger.debug(`Getting state for ${payload.entity}`); this.logger.debug(`Getting state for ${payload.entity}`);
const hass = connections[payload.identifier].hass; const hass = this.connections[payload.identifier].hass;
const [domain, entity] = payload.entity.split("."); const [domain, entity] = payload.entity.split(".");
const response = await hass.states.get(domain, entity); const response = await hass.states.get(domain, entity);
this.logger.debug(`Got state for ${payload.entity}`); this.logger.debug(`Got state for ${payload.entity}`);
@ -80,24 +95,33 @@ async function getState(payload) {
data: response, data: response,
}); });
if (!connections[payload.identifier].entities.includes(payload.entity)) { if (!this.connections[payload.identifier].entities.includes(payload.entity)) {
connections[payload.identifier].entities.push(payload.entity); this.connections[payload.identifier].entities.push(payload.entity);
} }
} }
async function toggleState(payload) { async function toggleState(payload) {
this.logger.debug(`Toggling state for ${payload.entity}`); 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 [domain, entity] = payload.entity.split(".");
const response = await hass.services.call('toggle', domain, entity) const response = await hass.services.call("toggle", domain, entity);
this.logger.debug(`Response for toggling state of ${payload.entity}`, response) this.getState(payload);
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) { function onStateChangedEvent(event) {
//this.logger.debug(`Got state change for ${event.data.entity_id}`); //this.logger.debug(`Got state change for ${event.data.entity_id}`);
for (const connection in connections) { for (const connection in this.connections) {
if (connections[connection].entities.includes(event.data.entity_id)) { if (this.connections[connection].entities.includes(event.data.entity_id)) {
this.logger.debug( this.logger.debug(
`Found listening connection (${connection}) for entity ${event.data.entity_id}` `Found listening connection (${connection}) for entity ${event.data.entity_id}`
); );