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:
+
{
- "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"
}
{
- "key": "change",
- "id": "entity_id"
- "state": "entity_state"
+ "key": "set", "id": "entity_id", "state": "entity_state"
}
fetchEntity(entity)fetchEntity(entity)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"
}