diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a1f234..1c76ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added +- Entity module to encapsulate data storage + +### Changed +- Fixed settings by moving code to companion +- API refactoring + ## Version 1.0 ### Added diff --git a/app/i18n/de-DE.po b/app/i18n/de-DE.po index 34566d7..0f36949 100644 --- a/app/i18n/de-DE.po +++ b/app/i18n/de-DE.po @@ -2,6 +2,9 @@ msgid "unavailable" msgstr "N/A" +msgid "loading" +msgstr "Lade..." + msgid "on" msgstr "AN" diff --git a/app/i18n/en-US.po b/app/i18n/en-US.po index 7becd22..1021ed3 100644 --- a/app/i18n/en-US.po +++ b/app/i18n/en-US.po @@ -2,6 +2,9 @@ msgid "unavailable" msgstr "N/A" +msgid "loading" +msgstr "Loading..." + msgid "on" msgstr "ON" diff --git a/app/i18n/it-IT.po b/app/i18n/it-IT.po index 12d503b..97f475e 100644 --- a/app/i18n/it-IT.po +++ b/app/i18n/it-IT.po @@ -2,6 +2,9 @@ msgid "unavailable" msgstr "N/D" +msgid "loading" +msgstr "Caricare..." + msgid "on" msgstr "ACC" @@ -10,3 +13,15 @@ msgstr "SPE" msgid "exe" msgstr "ESE" + +msgid "open" +msgstr "APE" + +msgid "opening" +msgstr "APE" + +msgid "closing" +msgstr "CHI" + +msgid "closed" +msgstr "CHI" diff --git a/app/index.js b/app/index.js index 64aeae4..aa7d295 100644 --- a/app/index.js +++ b/app/index.js @@ -4,24 +4,18 @@ import * as messaging from "messaging"; import { me } from "appbit"; import { gettext } from "i18n"; -import { settingsType, settingsFile } from "../common/constants"; import { sendData } from "../common/utils"; import document from "document"; -let Available = false; const EntityList = document.getElementById("entityList"); const AddressText = document.getElementById("addressText"); -AddressText.text = gettext("unavailable"); - -// Load settings -let settings = loadSettings(); - -// Register for the unload event -me.onunload = saveSettings; +AddressText.text = gettext("loading"); // List of {id: "", name: "", state: ""} let Entities = []; + +// List of state change by touch event const NextStates = { on: "turn_off", off: "turn_on", @@ -47,29 +41,28 @@ function setupList(list, data) { tile.getElementById("itemText").text = `${info.name}`; tile.getElementById("itemState").text = `${gettext(info.state)}`; let touch = tile.getElementById("itemTouch"); - touch.onclick = () => sendData({key: "change", entity: Entities[info.index].id, state: NextStates[info.state]}); + touch.onclick = () => sendData({key: "set", entity: Entities[info.index].id, state: NextStates[info.state]}); } } }; list.length = data.length; } -// Received message +// Received message from companion / HomeAssistantAPI messaging.peerSocket.onmessage = (evt) => { console.log(`Received: ${JSON.stringify(evt)}`); - if (evt.data.key === "clear") { + if (evt.data.key === "update" && evt.data.value === "begin") { Entities = []; - settings.entities = []; } else if (evt.data.key === "add") { Entities.push({id: evt.data.id, name: evt.data.name, state: evt.data.state}); - settings.entities.push({name: evt.data.id}); + } + else if (evt.data.key === "update" && evt.data.value === "end") { setupList(EntityList, Entities); } - else if (evt.data.key === "change") { + else if (evt.data.key === "set") { Entities.forEach((entity, index) => { if (entity.id === evt.data.id) { - //DEBUG console.log(`Updated: ${evt.data.id} to ${evt.data.state}`); Entities[index].state = evt.data.state; setupList(EntityList, Entities); } @@ -77,65 +70,21 @@ messaging.peerSocket.onmessage = (evt) => { } else if (evt.data.key === "api") { if (evt.data.value === "ok") { - Available = true; AddressText.text = evt.data.name; } else { - Available = false; AddressText.text = evt.data.value; } } - else if (evt.data.key === "url") { - settings.url = evt.data.value; - sendData({key: "url", value: settings.url}); - } - else if (evt.data.key === "port") { - settings.port = evt.data.value; - sendData({key: "port", value: settings.port}); - } - else if (evt.data.key === "token") { - settings.token = evt.data.value; - sendData({key: "token", value: settings.token}); - } - else if (evt.data.key === "force") { - settings.force = evt.data.value; - sendData({key: "force", value: settings.force}); - } } // Message socket opens messaging.peerSocket.onopen = () => { - console.log("Socket open"); - sendData({key: "url", value: settings.url}); - sendData({key: "port", value: settings.port}); - sendData({key: "token", value: settings.token}); - sendData({key: "entities", value: settings.entities}); - sendData({key: "force", value: settings.force}); + console.log("App socket open"); + sendData({key: "refresh"}); }; // Message socket closes messaging.peerSocket.onclose = () => { - console.log("Socket closed"); + console.log("App socket closed"); }; - -// Load settings -function loadSettings() { - try { - return fs.readFileSync(settingsFile, settingsType); - } - catch (ex) { - console.error("Error loading settings"); - // Default values - return { - url: "localhost", - port: "8123", - token: "", - force: true - }; - } -} - -// Save settings -function saveSettings() { - fs.writeFileSync(settingsFile, settings, settingsType); -} diff --git a/companion/Entity.js b/companion/Entity.js new file mode 100644 index 0000000..840c654 --- /dev/null +++ b/companion/Entity.js @@ -0,0 +1,130 @@ +/** + * @module Entity + * @brief Provides container for HA entities + */ + + +/** + * Create Entity class object + * @param {string} id - Entity ID + * @param {string} name - Entity name + * @param {string] state - Entity state + */ +export function Entity(id, name, state) { + this.id = id; + this.name = name; + this.state = state; +} + +/** + * Entity validity + * @return True if the entity has a valid id and a valid state + */ +Entity.prototype.isValid = function() { + let self = this; + return self.id !== undefined && self.id !== "" && self.state !== undefined && self.state !== ""; +} + +/** + * Create Entities class object + */ +export function Entities() { + this.list = []; +} + +/** + * Clear entity list + */ +Entities.prototype.clear = function() { + let self = this; + self.list = []; +} + +/** + * Add new entity + * @param {string} id - Entity id + * @param {string} name - Entity name + * @param {string} state - Entity state + */ +Entities.prototype.add = function(id, name, state) { + let self = this; + if (self.findById(id) === -1) { + let entity = new Entity(id, name, state); + if (entity.isValid()) { + self.list.push(entity); + } + } +} + +/** + * Remove entity + * @param {string} id - Entity id + */ +Entities.prototype.remove = function(id) { + let self = this; + let index = self.findById(id); + if (index !== -1) { + self.list.splice(index, 1); + } +} + +/** + * Find entity by its id + * @param {string} id - Entity id + * @return Entity index or -1 if entity wasn't found + */ +Entities.prototype.findById = function(id) { + let self = this; + self.list.forEach((entity, index) => { + if (entity.id === id) { + return index; + } + }) + return -1; +} + +/** + * Find entity by its name + * @param {string} name - Entity name + * @return Entity index or -1 if entity wasn't found + */ +Entities.prototype.findByName = function(name) { + let self = this; + self.list.forEach((entity, index) => { + if (entity.name === name) { + return index; + } + }) + return -1; +} + +/** + * Set state of an entity + * @param {string} id - Entity id + * @param {string} state - Entity state + */ +Entities.prototype.set = function(id, state) { + let self = this; + let index = self.findById(id); + if (index !== -1) { + self.list[index].state = state; + } +} + +/** + * Sort entities by ids + */ +Entities.prototype.sort = function() { + let self = this; + self.list.sort(function(entityA, entityB) { + let idA = entityA.id.toUpperCase(); + let idB = entityB.id.toUpperCase(); + if (idA < idB) { + return -1; + } + else if (idA > idB) { + return 1; + } + return 0; + }) +} diff --git a/companion/HomeAssistantAPI.js b/companion/HomeAssistantAPI.js index 569c3f0..f1e2c80 100644 --- a/companion/HomeAssistantAPI.js +++ b/companion/HomeAssistantAPI.js @@ -4,6 +4,7 @@ */ import { gettext } from "i18n"; import { sendData, isEmpty } from "../common/utils"; +import { Entity, Entities } from "./Entity"; const Groups = { switch: "switch", @@ -36,11 +37,14 @@ export function HomeAssistantAPI() { this.port = ""; this.token = ""; this.force = false; + this.status = "loading"; + this.name = ""; + this.entities = new Entities(); } /** * Configuration validity - * @return True if configuration contains valid data, otherwise false. + * @return True if configuration contains valid data, otherwise false */ HomeAssistantAPI.prototype.isValid = function() { let self = this; @@ -49,11 +53,12 @@ HomeAssistantAPI.prototype.isValid = function() { } /** - * Configuration validity + * Setup configuration * @param {string} url - HomeAssistant instance URL * @param {string} port - HomeAssistant instance port * @param {string} token - Access token * @param {boolean} force - Force update flag + * @return True if configuration is valid */ HomeAssistantAPI.prototype.setup = function(url, port, token, force) { let self = this; @@ -61,11 +66,42 @@ HomeAssistantAPI.prototype.setup = function(url, port, token, force) { self.changePort(port); self.changeToken(token); self.changeForce(force); + return self.isValid(); +} + +/** + * Send update of api status and all entities + */ +HomeAssistantAPI.prototype.update = function() { + let self = this; + sendData({key: "api", value: self.status, name: self.name}); + sendData({key: "update", value: "begin"}); + self.entities.list.forEach((entity, index) => { + sendData({key: "add", index: index, id: entity.id, name: entity.name, state: entity.state}); + }); + sendData({key: "update", value: "end"}); +} + +/** + * Clear internal entity list + */ +HomeAssistantAPI.prototype.clear = function() { + let self = this; + self.entities.clear(); +} + +/** + * Sort internal entity list + */ +HomeAssistantAPI.prototype.sort = function() { + let self = this; + self.entities.sort(); } /** * Change URL - * @param {string} url - HomeAssistant instance URL + * @param {string} url - HomeAssistant instance URL + * @return True if configuration is valid */ HomeAssistantAPI.prototype.changeUrl = function(url) { let self = this; @@ -75,11 +111,13 @@ HomeAssistantAPI.prototype.changeUrl = function(url) { else { self.url = '127.0.0.1'; } + return self.isValid(); } /** * Change port number * @param {string} port - HomeAssistant instance port + * @return True if configuration is valid */ HomeAssistantAPI.prototype.changePort = function(port) { let self = this; @@ -89,11 +127,13 @@ HomeAssistantAPI.prototype.changePort = function(port) { else { self.port = '8123'; } + return self.isValid(); } /** * Change token * @param {string} token - Access token + * @return True if configuration is valid */ HomeAssistantAPI.prototype.changeToken = function(token) { let self = this; @@ -103,11 +143,13 @@ HomeAssistantAPI.prototype.changeToken = function(token) { else { self.token = ''; } + return self.isValid(); } /** * Change force update flag * @param {boolean} force - Force update flag + * @return True if configuration is valid */ HomeAssistantAPI.prototype.changeForce = function(force) { let self = this; @@ -117,6 +159,7 @@ HomeAssistantAPI.prototype.changeForce = function(force) { else { self.force = true; } + return self.isValid(); } /** @@ -129,8 +172,8 @@ HomeAssistantAPI.prototype.address = function() { } /** - * Fetch entity - * @param {string} entity - Entity name + * Fetch new entity + * @param {string} entity - Entity ID */ HomeAssistantAPI.prototype.fetchEntity = function(entity) { let self = this; @@ -146,7 +189,6 @@ HomeAssistantAPI.prototype.fetchEntity = function(entity) { if (response.ok) { let data = await response.json(); let msgData = { - key: "add", id: data["entity_id"], name: data["entity_id"], state: data["state"], @@ -157,7 +199,10 @@ HomeAssistantAPI.prototype.fetchEntity = function(entity) { if (self.isExecutable(data["entity_id"])) { msgData.state = 'exe' } - sendData(msgData); + //DEBUG console.log('ADDED ' + JSON.stringify(msgData)); + self.entities.add(msgData.id, msgData.name, msgData.state); + slef.entities.sort(); + self.update(); } else { console.log(`[fetchEntity] ${gettext("error")} ${response.status}`); @@ -183,26 +228,30 @@ HomeAssistantAPI.prototype.fetchApiStatus = function() { .then(async(response) => { let data = await response.json(); if (response.status === 200) { - sendData({key: "api", value: "ok", name: data["location_name"]}); + self.status = "ok"; + self.name = data["location_name"]; + sendData({key: "api", value: "ok", name: self.name}); } else { - const json = JSON.stringify({ - key: "api", - value: `${gettext("error")} ${response.status}` - }); - sendData(json); + self.status = `${gettext("error")} ${response.status}`; + sendData({key: "api", value: self.status}); } }) .catch(err => { console.log('[fetchApiStatus]: ' + err); - sendData({key: "api", value: gettext("connection_error")}); + self.status = gettext("connection_error"); + sendData({key: "api", value: self.status}); }) } + else { + self.status = gettext("invalid_config"); + sendData({key: "api", value: self.status}); + } } /** * Change entity - * @param {string} entity - Entity name + * @param {string} entity - Entity ID * @param {string} state - New state value */ HomeAssistantAPI.prototype.changeEntity = function(entity, state) { @@ -229,12 +278,13 @@ HomeAssistantAPI.prototype.changeEntity = function(entity, state) { //DEBUG console.log('RECEIVED ' + JSON.stringify(data)); if (self.force) { let msgData = { - key: "change", + key: "set", id: entity, state: ForcedStates[state] || state, }; if (!self.isExecutable(entity)) { //DEBUG console.log('FORCED ' + JSON.stringify(msgData)); + self.entities.set(msgData.id, msgData.state); sendData(msgData); } } @@ -242,11 +292,13 @@ HomeAssistantAPI.prototype.changeEntity = function(entity, state) { data.forEach(element => { if (element["entity_id"] === entity) { let msgData = { - key: "change", + key: "set", id: element["entity_id"], state: element["state"], }; if (!self.isExecutable(element["entity_id"])) { + //DEBUG console.log('UPDATED ' + JSON.stringify(msgData)); + self.entities.set(msgData.id, msgData.state); sendData(msgData); } } @@ -263,7 +315,7 @@ HomeAssistantAPI.prototype.changeEntity = function(entity, state) { /** * Returns if an entity is an executable - * @param {string} entity - Entity name + * @param {string} entity - Entity ID * @return True if entity is an executable, otherwise false */ HomeAssistantAPI.prototype.isExecutable = function(entity) { diff --git a/companion/README.md b/companion/README.md index e64bd39..dc7e786 100644 --- a/companion/README.md +++ b/companion/README.md @@ -48,18 +48,17 @@ The asynchronous answer will be packed as JSON in a socket message. fetchApiStatus() -Ok: +Ok: + { - "key": "api", - "value": "ok", - "name": "location_name" + "key": "api", "value": "ok", "name": "location_name" }
-Error: +Error: + { - "key": "api", - "value": "error_message" + "key": "api", "value": "error_message" } @@ -69,22 +68,30 @@ Error: { - "key": "change", - "id": "entity_id" - "state": "entity_state" + "key": "set", "id": "entity_id", "state": "entity_state" } - fetchEntity(entity) + fetchEntity(entity)
update() +Start update: + +{ + "key": "update", "value": "begin" +} + +Add entries: + +{ + "key": "add", "index": "index", "id": "entity_id", "name": "entity_name", "state": "entity_state" +} + +Update is done: { - "key": "add", - "id": "entity_id", - "name": "entity_name" - "state": "entity_state" + "key": "update", "value": "end" } @@ -106,9 +113,11 @@ Companion import { HomeAssistantAPI } from "./HomeAssistantAPI"; var HA = new HomeAssistantAPI(); -HA.setup("127.0.0.1", "8123", "my_secret_access_token", false); -HA.fetchApiStatus(); -HA.fetchEntity("switch.myswitch"); +if (HA.setup("127.0.0.1", "8123", "my_secret_access_token", false)) { + HA.fetchApiStatus(); + HA.fetchEntity("switch.myswitch"); + HA.changeEntity("switch.myswitch", "off"); +} ``` App @@ -125,8 +134,17 @@ messaging.peerSocket.onmessage = (evt) => { console.log(`Error: ${evt.data.value}`); } } + else if (evt.data.key === "update" && evt.data.value === "begin") { + console.log(`Start update`); + } else if (evt.data.key === "add") { console.log(`New entry: ${evt.data.name} = ${evt.data.state}`); } + else if (evt.data.key === "update" && evt.data.value === "end") { + console.log(`End update`); + } + else if (evt.data.key === "set") { + console.log(`Set entry: ${evt.data.name} = ${evt.data.state}`); + } } ``` diff --git a/companion/i18n/de-DE.po b/companion/i18n/de-DE.po index fe0c4e4..7999d73 100644 --- a/companion/i18n/de-DE.po +++ b/companion/i18n/de-DE.po @@ -9,4 +9,7 @@ msgid "response_error" msgstr "Antwortfehler" msgid "error" -msgstr "Fehler" \ No newline at end of file +msgstr "Fehler" + +msgid "invalid_config" +msgstr "Ungültige Konfig." \ No newline at end of file diff --git a/companion/i18n/en-US.po b/companion/i18n/en-US.po index 23f367a..cf702c2 100644 --- a/companion/i18n/en-US.po +++ b/companion/i18n/en-US.po @@ -9,4 +9,7 @@ msgid "response_error" msgstr "Response error" msgid "error" -msgstr "Error" \ No newline at end of file +msgstr "Error" + +msgid "invalid_config" +msgstr "Invalid config." \ No newline at end of file diff --git a/companion/i18n/it-IT.po b/companion/i18n/it-IT.po index 95fd396..f9e5efc 100644 --- a/companion/i18n/it-IT.po +++ b/companion/i18n/it-IT.po @@ -4,3 +4,12 @@ msgstr "Non disponibile" msgid "connection_error" msgstr "Errore di connessione" + +msgid "response_error" +msgstr "Errore di risposta" + +msgid "error" +msgstr "Errore" + +msgid "invalid_config" +msgstr "Config. non valida" \ No newline at end of file diff --git a/companion/index.js b/companion/index.js index 3ab40cf..7615e2b 100644 --- a/companion/index.js +++ b/companion/index.js @@ -11,82 +11,102 @@ import { HomeAssistantAPI } from "./HomeAssistantAPI"; // Create HomeAssistantAPI object var HA = new HomeAssistantAPI(); +// Load settings +let settings = loadSettings(); +applySettings(); + +// Register for the unload event +companion.onunload = saveSettings; + // Settings have been changed settingsStorage.onchange = function(evt) { - if (evt.key === "url") { - let data = JSON.parse(evt.newValue); - sendData({key: "url", value: data["name"]}); - } - else if (evt.key === "port") { - let data = JSON.parse(evt.newValue); - sendData({key: "port", value: data["name"]}); - } - else if (evt.key === "token") { - let data = JSON.parse(evt.newValue); - sendData({key: "token", value: data["name"]}); - } - else if (evt.key === "entities") { - sendData({key: "clear"}); + if (evt.key === "entities") { + HA.clear(); JSON.parse(evt.newValue).forEach(element => { HA.fetchEntity(element["name"]); }); + HA.sort(); + HA.update(); } - else if (evt.key === "force") { + else { let data = JSON.parse(evt.newValue); - sendData({key: "force", value: data}); + if (evt.key === "url") { + HA.changeUrl(data["name"]); + } + else if (evt.key === "port") { + HA.changePort(data["name"]); + } + else if (evt.data === "token") { + HA.changeToken(data["name"]); + } + else if (evt.key === "force") { + HA.changeForce(data); + } + if (HA.isValid()) { + HA.fetchApiStatus(); + } } } // Settings changed while companion was not running if (companion.launchReasons.settingsChanged) { - const keys = ["url", "port", "token", "force"]; - keys.forEach(function(keyName, index, array) { - sendData({key: keyName, value: settingsStorage.getItem(keyName)}); - }); - sendData({key: "clear"}); - JSON.parse(settingsStorage?.getItem("entities"))?.forEach((element) => { - HA.fetchEntity(element["name"]); - }); + applySettings(); } // Message socket opens messaging.peerSocket.onopen = () => { - console.log('Socket open'); + console.log('Companion socket open'); }; // Message socket closes messaging.peerSocket.onclose = () => { - console.log('Socket closed'); + console.log('Companion socket closed'); }; // Received message from App messaging.peerSocket.onmessage = evt => { console.log('Received', JSON.stringify(evt.data)); - if (evt.data.key === "change") { + if (evt.data.key === "set") { HA.changeEntity(evt.data.entity, evt.data.state); } - else if (evt.data.key === "url") { - HA.changeUrl(evt.data.value); - HA.fetchApiStatus(); + else if (evt.data.key === "refresh") { + HA.update(); } - else if (evt.data.key === "port") { - HA.changePort(evt.data.value); - HA.fetchApiStatus(); +}; + +// Load settings +function loadSettings() { + try { + return fs.readFileSync(settingsFile, settingsType); } - else if (evt.data.key === "token") { - HA.changeToken(evt.data.value); - HA.fetchApiStatus(); + catch (ex) { + console.error("Error loading settings"); + // Default values + return { + url: "localhost", + port: "8123", + token: "", + force: true + }; } - else if (evt.data.key === "entities") { - if (evt.data.value) { - sendData({key: "clear"}); - evt.data.value.forEach(element => { - HA.fetchEntity(element["name"]); - }) - } - } - else if (evt.data.key === "force") { - HA.changeForce(evt.data.value); +} + +// Save settings +function saveSettings() { + fs.writeFileSync(settingsFile, settingsStorage, settingsType); +} + +// Apply settings +function applySettings() { + if (HA.setup(settingsStorage.getItem("url"), settingsStorage.getItem("port"), + settingsStorage.getItem("token"), settingsStorage.getItem("force"))) { HA.fetchApiStatus(); + + HA.clear(); + JSON.parse(settingsStorage?.getItem("entities"))?.forEach((element) => { + HA.fetchEntity(element["name"]); + }); + HA.sort(); + HA.update(); } -}; +} diff --git a/resources/styles.sdk5.css b/resources/styles.sdk6.css similarity index 100% rename from resources/styles.sdk5.css rename to resources/styles.sdk6.css diff --git a/resources/widget.defs b/resources/widget.defs index d166990..d08c1a3 100644 --- a/resources/widget.defs +++ b/resources/widget.defs @@ -1,6 +1,6 @@ - +