-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3558 from flaparoo/hadash
hadash: initial release
- Loading branch information
Showing
11 changed files
with
39,654 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
hadash.json | ||
node_modules | ||
package-lock.json | ||
package.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
1.00: initial release |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Home-Assistant Dashboard | ||
|
||
This app interacts with a Home-Assistant (HA) instance. You can query entity | ||
states and call services. This allows you access to up-to-date information of | ||
any home automation system integrated into HA, and you can also control your | ||
automations from your wrist. | ||
|
||
![](screenshot.png) | ||
|
||
|
||
## How It Works | ||
|
||
This app uses the REST API to directly interact with HA (which requires a | ||
"long-lived access token" - refer to "Configuration"). | ||
|
||
You can define a menu structure to be displayed on your Bangle, with the states | ||
to be queried and services to be called. Menu entries can be: | ||
|
||
* entry to show the state of a HA entity | ||
* entry to call a HA service | ||
* sub-menus, including nested sub-menus | ||
|
||
Calls to a service can also have optional input for data fields on the Bangle | ||
itself. | ||
|
||
|
||
## Configuration | ||
|
||
After installing the app, use the "interface" page (floppy disk icon) in the | ||
App Loader to configure it. | ||
|
||
Make sure to set the "Home-Assistant API Base URL" (which must include the | ||
"/api" path, as well - but no slash at the end). | ||
|
||
Also create a "long-lived access token" in HA (under the Profile section, at | ||
the bottom) and enter it as the "Long-lived access token". | ||
|
||
The tricky bit will be to configure your menu structure. You need to have a | ||
basic understanding of the JSON format. The configuration page uses a JSON | ||
Editor which will check the syntax and highlight any errors for you. Follow the | ||
instructions on the page regarding how to configure menus, menu entries and the | ||
required attributes. It also contains examples. | ||
|
||
Once you're happy with the menu structure (and you've entered the base URL and | ||
access token), click the "Configure / Upload to Bangle" button. | ||
|
||
|
||
## Security | ||
|
||
The "long-lived access token" will be stored unencrypted on your Bangle. This | ||
would - in theory - mean that if your Bangle gets stolen, the new "owner" would | ||
have unrestricted access to your Home-Assistant instance (the thief would have | ||
to be fairly tech-savvy, though). However, I suggest you create a separate | ||
token exclusively for your Bangle - that way, it's very easy to simply delete | ||
that token in case your watch is stolen or lost. | ||
|
||
|
||
## To-Do | ||
|
||
- A better way to configure the menu structure would be useful, something like a custom editor (replacing the jsoneditor). | ||
- After showing a state or call a service, return to the same point in the menu. | ||
- Config option for service calls to not show a "successful" prompt | ||
|
||
|
||
## Author | ||
|
||
Flaparoo [github](https://github.com/flaparoo) | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
/* | ||
* Home-Assistant Dashboard - Bangle.js | ||
*/ | ||
|
||
const APP_NAME = 'hadash'; | ||
|
||
// Load settings | ||
var settings = Object.assign({ | ||
menu: [ | ||
{ type: 'state', title: 'Check for updates', id: 'update.home_assistant_core_update' }, | ||
{ type: 'service', title: 'Create Notification', domain: 'persistent_notification', service: 'create', | ||
data: { 'message': 'test notification', 'title': 'Test'} }, | ||
{ type: 'menu', title: 'Sub-menu', data: | ||
[ | ||
{ type: 'state', title: 'Check for Supervisor updates', id: 'update.home_assistant_supervisor_update' }, | ||
{ type: 'service', title: 'Restart HA', domain: 'homeassistant', service: 'restart', data: {} } | ||
] | ||
}, | ||
{ type: 'service', title: 'Custom Notification', domain: 'persistent_notification', service: 'create', | ||
data: { 'title': 'Not via input'}, | ||
input: { 'message': { options: [], value: 'Pre-filled text' }, | ||
'notification_id': { options: [ 123, 456, 136 ], value: 999, label: "ID" } } }, | ||
], | ||
HAbaseUrl: '', | ||
HAtoken: '', | ||
}, require('Storage').readJSON(APP_NAME+'.json', true) || {}); | ||
|
||
|
||
// query an entity state | ||
function queryState(title, id, level) { | ||
E.showMessage('Fetching entity state from HA', { title: title }); | ||
Bangle.http(settings.HAbaseUrl+'/states/'+id, { | ||
headers: { | ||
'Authorization': 'Bearer '+settings.HAtoken, | ||
'Content-Type': 'application/json' | ||
}, | ||
}).then(data => { | ||
//console.log(data); | ||
let HAresp = JSON.parse(data.resp); | ||
let title4prompt = title; | ||
let msg = HAresp.state; | ||
if ('attributes' in HAresp) { | ||
if ('friendly_name' in HAresp.attributes) | ||
title4prompt = HAresp.attributes.friendly_name; | ||
if ('unit_of_measurement' in HAresp.attributes) | ||
msg += HAresp.attributes.unit_of_measurement; | ||
} | ||
E.showPrompt(msg, { title: title4prompt, buttons: {OK: true} }).then((v) => { E.showMenu(menus[level]); }); | ||
}).catch( error => { | ||
console.log(error); | ||
E.showPrompt('Error querying state!', { title: title, buttons: {OK: true} }).then((v) => { E.showMenu(menus[level]); }); | ||
}); | ||
} | ||
|
||
|
||
// call a service | ||
function callService(title, domain, service, data, level) { | ||
E.showMessage('Calling HA service', { title: title }); | ||
Bangle.http(settings.HAbaseUrl+'/services/'+domain+'/'+service, { | ||
method: 'POST', | ||
body: data, | ||
headers: { | ||
'Authorization': 'Bearer '+settings.HAtoken, | ||
'Content-Type': 'application/json' | ||
}, | ||
}).then(data => { | ||
//console.log(data); | ||
E.showPrompt('Service called successfully', { title: title, buttons: {OK: true} }).then((v) => { E.showMenu(menus[level]); }); | ||
}).catch( error => { | ||
console.log(error); | ||
E.showPrompt('Error calling service!', { title: title, buttons: {OK: true} }).then((v) => { E.showMenu(menus[level]); }); | ||
}); | ||
} | ||
|
||
|
||
// callbacks for service input menu entries | ||
function serviceInputChoiceChange(v, key, entry, level) { | ||
entry.input[key].value = entry.input[key].options[v]; | ||
getServiceInputData(entry, level); | ||
} | ||
|
||
function serviceInputFreeform(key, entry, level) { | ||
require("textinput").input({text: entry.input[key].value}).then(result => { | ||
entry.input[key].value = result; | ||
getServiceInputData(entry, level); | ||
}); | ||
} | ||
|
||
// get input data before calling a service | ||
function getServiceInputData(entry, level) { | ||
let serviceInputMenu = { | ||
'': { | ||
'title': entry.title, | ||
'back': () => E.showMenu(menus[level]) | ||
}, | ||
}; | ||
let CBs = {}; | ||
for (let key in entry.input) { | ||
// pre-fill data with default values | ||
if ('value' in entry.input[key]) | ||
entry.data[key] = entry.input[key].value; | ||
|
||
let label = ( ('label' in entry.input[key] && entry.input[key].label) ? entry.input[key].label : key ); | ||
let key4CB = key; | ||
|
||
if ('options' in entry.input[key] && entry.input[key].options.length) { | ||
// give choice from a selection of options | ||
let idx = -1; | ||
for (let i in entry.input[key].options) { | ||
if (entry.input[key].value == entry.input[key].options[i]) { | ||
idx = i; | ||
} | ||
} | ||
if (idx == -1) { | ||
idx = entry.input[key].options.push(entry.input[key].value) - 1; | ||
} | ||
// the setTimeout method can not be used for the "format" CB since it expects a return value: | ||
CBs[`${key}_format`] = ((key) => function(v) { return entry.input[key].options[v]; })(key); | ||
serviceInputMenu[label] = { | ||
value: parseInt(idx), | ||
min: 0, | ||
max: entry.input[key].options.length - 1, | ||
format: CBs[key+'_format'], | ||
onchange: (v) => setTimeout(serviceInputChoiceChange, 10, v, key4CB, entry, level) | ||
}; | ||
|
||
} else { | ||
// free-form text input | ||
serviceInputMenu[label] = () => setTimeout(serviceInputFreeform, 10, key4CB, entry, level); | ||
} | ||
} | ||
// menu entry to actually call the service: | ||
serviceInputMenu['Call service'] = function() { callService(entry.title, entry.domain, entry.service, entry.data, level); }; | ||
E.showMenu(serviceInputMenu); | ||
} | ||
|
||
|
||
// menu hierarchy | ||
var menus = []; | ||
|
||
|
||
// add menu entries | ||
function addMenuEntries(level, entries) { | ||
for (let i in entries) { | ||
let entry = entries[i]; | ||
let entryCB; | ||
|
||
// is there a menu entry title? | ||
if (! ('title' in entry) || ! entry.title) | ||
entry.title = 'TBD'; | ||
|
||
switch (entry.type) { | ||
case 'state': | ||
/* | ||
* query entity state | ||
*/ | ||
if ('id' in entry && entry.id) { | ||
entryCB = () => setTimeout(queryState, 10, entry.title, entry.id, level); | ||
} | ||
break; | ||
|
||
case 'service': | ||
/* | ||
* call HA service | ||
*/ | ||
if ('domain' in entry && entry.domain && 'service' in entry && entry.service) { | ||
if (! ('data' in entry)) | ||
entry.data = {}; | ||
if ('input' in entry) { | ||
// get input for some data fields first | ||
entryCB = () => setTimeout(getServiceInputData, 10, entry, level); | ||
} else { | ||
// call service straight away | ||
entryCB = () => setTimeout(callService, 10, entry.title, entry.domain, entry.service, entry.data, level); | ||
} | ||
} | ||
break; | ||
|
||
case 'menu': | ||
/* | ||
* sub-menu | ||
*/ | ||
entryCB = () => setTimeout(showSubMenu, 10, level + 1, entry.title, entry.data); | ||
break; | ||
} | ||
|
||
// only attach a call-back to menu entry if it's properly configured | ||
if (! entryCB) { | ||
menus[level][entry.title + ' - not correctly configured!'] = {}; | ||
} else { | ||
menus[level][entry.title] = entryCB; | ||
} | ||
} | ||
} | ||
|
||
|
||
// create and show a sub menu | ||
function showSubMenu(level, title, entries) { | ||
menus[level] = { | ||
'': { | ||
'title': title, | ||
'back': () => E.showMenu(menus[level - 1]) | ||
}, | ||
}; | ||
addMenuEntries(level, entries); | ||
E.showMenu(menus[level]); | ||
} | ||
|
||
|
||
/* | ||
* create the main menu | ||
*/ | ||
menus[0] = { | ||
'': { | ||
'title': 'HA-Dash', | ||
'back': () => load() | ||
}, | ||
}; | ||
addMenuEntries(0, settings.menu); | ||
|
||
// check required configuration | ||
if (! settings.HAbaseUrl || ! settings.HAtoken) { | ||
E.showAlert('The app is not yet configured!', 'HA-Dash').then(() => E.showMenu(menus[0])); | ||
} else { | ||
E.showMenu(menus[0]); | ||
} | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.