diff --git a/apps/bthome/ChangeLog b/apps/bthome/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/bthome/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/bthome/README.md b/apps/bthome/README.md new file mode 100644 index 000000000..d232e8d64 --- /dev/null +++ b/apps/bthome/README.md @@ -0,0 +1,26 @@ +# BTHome + +This uses BTHome (https://bthome.io/) to allow easy control of [Home Assistant](https://www.home-assistant.io/) via Bluetooth advertisements. + +Other apps like [the Home Assistant app](https://banglejs.com/apps/?id=ha) communicate with Home Assistant +via your phone so work from anywhere, but require being in range of your phone. + +## Usage + +When the app is installed, go to the `BTHome` app and click Settings. + +Here, you can choose if you want to advertise your Battery status, but can also click `Add Button`. + +You can then add a custom button event: + +* `Icon` - the picture for the button +* `Name` - the name associated with the button +* `Action` - the action that Home Assistant will see when this button is pressed +* `Button #` - the button event 'number' - keep this at 0 for now + +Once you've saved, you will then get your button shown in the BTHome app. Tapping it will make Bangle.js advertise via BTHome that the button has been pressed. + +## ClockInfo + +When you've added one or more buttons, they will appear in a ClockInfo under the main `Bangle.js` heading. You can just tap to select the ClockInfo, scroll down until a BTHome one is visible and then tap again. It will immediately send the Advertisement. + diff --git a/apps/bthome/app-icon.js b/apps/bthome/app-icon.js new file mode 100644 index 000000000..ecdc205bc --- /dev/null +++ b/apps/bthome/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAIHBy06nnnDiHwBRMDrgLJhtXBZM1qvABZHVqtwFxFVqowIhoLBGBE1q35GBHVrkDytyrAuGHIPVroLFFwODrklqoLFLoOCrALHLoIXILoVw+APBBYhdCsEAyphFFwITBgQDBMIgeBqtUgILCSQQuBrflBYW+SQYuBuENBYItB6owCXYUDBYIUBYYYuBh2wBYNQ9cFGAWlq0JsGUgNgy0J1WsEgMWhtwBYXXhWq1YLBkvD4HUgNwnk61Wq2ALBwEAkkBAYPq14kCktsgEMgZmBBIILDqoMBBQOWBIM61ALCrYLBh1WBYMKHgILBqxlBnILC2eqBYVVIAPlrWj1mg9QLDtkDyta1ns2AXEX4Va1c84YLEWYVa1XAhwLJ2B5BBZA6BBZOAC5UA5xHI1E8NYQAFh2g9hrCBY2vQYYAFgSPBF4QAFX4U6cgQLH9S/BAA2qcYYAG9WuPIILHOoKdBBY8D9WvgA")) \ No newline at end of file diff --git a/apps/bthome/app.js b/apps/bthome/app.js new file mode 100644 index 000000000..7e9a39531 --- /dev/null +++ b/apps/bthome/app.js @@ -0,0 +1,27 @@ +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +function showMenu() { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + var menu = { "": {title:"BTHome", back:load} }; + settings.buttons.forEach((button,idx) => { + var img = require("icons").getIcon(button.icon); + menu[/*LANG*/"\0"+img+" "+button.name] = function() { + Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); + E.showMenu(); + E.showMessage("Sending Event"); + Bangle.buzz(); + setTimeout(showMenu, 500); + }; + }); + menu[/*LANG*/"Settings"] = function() { + eval(require("Storage").read("bthome.settings.js"))(()=>showMenu()); + }; + E.showMenu(menu); +} + +showMenu(); + + diff --git a/apps/bthome/boot.js b/apps/bthome/boot.js new file mode 100644 index 000000000..9c02581fe --- /dev/null +++ b/apps/bthome/boot.js @@ -0,0 +1,68 @@ +// Ensure we have the bleAdvert global (to play well with other stuff) +if (!Bangle.bleAdvert) Bangle.bleAdvert = {}; +Bangle.btHomeData = []; +{ + require("BTHome").packetId = 0|(Math.random()*256); // random packet id so new packets show up + let settings = require("Storage").readJSON("bthome.json",1)||{}; + if (settings.showBattery) + Bangle.btHomeData.push({ + type : "battery", + v : E.getBattery() + }); + // If buttons defined, add events for them + if (settings.buttons instanceof Array) { + let n = settings.buttons.reduce((n,b)=>b.n>n?b.n:n,-1); + for (var i=0;i<=n;i++) + Bangle.btHomeData.push({type:"button_event",v:"none",n:n}); + } +} + +/* Global function to allow advertising BTHome adverts + extras = array of extra data, see require("BTHome").getAdvertisement - can add {n:0/1/2} for different instances + options = { event : an event - advertise fast, and when connected + } +*/ +Bangle.btHome = function(extras, options) { + options = options||{}; + if(extras) { // update with extras + extras.forEach(extra => { + var n = Bangle.btHomeData.find(b=>b.type==extra.type && b.n==extra.n); + if (n) Object.assign(n, extra); + else Bangle.btHomeData.push(extra); + }); + } + var bat = Bangle.btHomeData.find(b=>b.type=="battery"); + if (bat) bat.v = E.getBattery(); + var advert = require("BTHome").getAdvertisement(Bangle.btHomeData)[0xFCD2]; + // Add to the list of available advertising + if(Array.isArray(Bangle.bleAdvert)){ + var found = false; + for(var ad in Bangle.bleAdvert){ + if(ad[0xFCD2]){ + ad[0xFCD2] = advert; + found = true; + break; + } + } + if(!found) + Bangle.bleAdvert.push({ 0xFCD2: advert }); + } else { + Bangle.bleAdvert[0xFCD2] = advert; + } + var advOptions = {}; + var updateTimeout = 10*60*1000; // update every 10 minutes + if (options.event) { // if it's an event... + advOptions.interval = 50; + advOptions.whenConnected = true; + updateTimeout = 30000; // slow down in 30 seconds + } + NRF.setAdvertising(Bangle.bleAdvert, advOptions); + if (Bangle.btHomeTimeout) clearTimeout(Bangle.btHomeTimeout); + Bangle.btHomeTimeout = setTimeout(function() { + delete Bangle.btHomeTimeout; + // clear events + Bangle.btHomeData.forEach(d => {if (d.type=="button_event") d.v="none";}); + // update + Bangle.btHome(); + },updateTimeout); +}; diff --git a/apps/bthome/clkinfo.js b/apps/bthome/clkinfo.js new file mode 100644 index 000000000..8698c9828 --- /dev/null +++ b/apps/bthome/clkinfo.js @@ -0,0 +1,17 @@ +(function() { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + return { + name: "Bangle", + items: settings.buttons.map(button => { + return { name : button.name, + get : function() { return { text : button.name, + img : require("icons").getIcon(button.icon) }}, + show : function() {}, + hide : function() {}, + run : function() { Bangle.btHome([{type:"button_event",v:button.v,n:button.n}],{event:true}); } + } + }) + }; +}) // must not have a semi-colon! \ No newline at end of file diff --git a/apps/bthome/icon.png b/apps/bthome/icon.png new file mode 100644 index 000000000..091784477 Binary files /dev/null and b/apps/bthome/icon.png differ diff --git a/apps/bthome/metadata.json b/apps/bthome/metadata.json new file mode 100644 index 000000000..643e24a63 --- /dev/null +++ b/apps/bthome/metadata.json @@ -0,0 +1,20 @@ +{ "id": "bthome", + "name": "BTHome", + "shortName":"BTHome", + "version":"0.01", + "description": "Allow your Bangle to advertise with BTHome and send events to Home Assistant via Bluetooth", + "icon": "icon.png", + "type": "app", + "tags": "clkinfo,bthome,bluetooth", + "supports" : ["BANGLEJS2"], + "dependencies": {"textinput":"type"}, + "readme": "README.md", + "storage": [ + {"name":"bthome.img","url":"app-icon.js","evaluate":true}, + {"name":"bthome.clkinfo.js","url":"clkinfo.js"}, + {"name":"bthome.boot.js","url":"boot.js"}, + {"name":"bthome.app.js","url":"app.js"}, + {"name":"bthome.settings.js","url":"settings.js"} + ], + "data":[{"name":"bthome.json"}] +} diff --git a/apps/bthome/settings.js b/apps/bthome/settings.js new file mode 100644 index 000000000..70f50f2ac --- /dev/null +++ b/apps/bthome/settings.js @@ -0,0 +1,91 @@ +(function(back) { + var settings = require("Storage").readJSON("bthome.json",1)||{}; + if (!(settings.buttons instanceof Array)) + settings.buttons = []; + + function saveSettings() { + require("Storage").writeJSON("bthome.json",settings) + } + + function showButtonMenu(button, isNew) { + var isNew = false; + if (!button) { + button = {name:"home", icon:"home", n:0, v:"press"}; + isNew = true; + } + var actions = ["press","double_press","triple_press","long_press","long_double_press","long_triple_press"]; + var menu = { + "":{title:isNew ? /*LANG*/"New Button" : /*LANG*/"Edit Button", back:showMenu}, + /*LANG*/"Icon" : { + value : "\0"+require("icons").getIcon(button.icon), + onchange : () => { + require("icons").showIconChooser().then(function(iconName) { + button.icon = iconName; + button.name = iconName; + showButtonMenu(button, isNew); + }, function() { + showButtonMenu(button, isNew); + }); + } + }, + /*LANG*/"Name" : { + value : button.name, + onchange : () => { + require("textinput").input({text:button.name}).then(function(name) { + button.name = name; + showButtonMenu(button, isNew); + }, function() { + showButtonMenu(button, isNew); + }); + } + }, + /*LANG*/"Action" : { + value : Math.max(0,actions.indexOf(button.v)), min:0, max:actions.length-1, + format : v => actions[v], + onchange : v => button.v=actions[v] + }, + /*LANG*/"Button #" : { + value : button.n, min:0, max:3, + onchange : v => button.n=v + }, + /*LANG*/"Save" : () => { + settings.buttons.push(button); + saveSettings(); + showMenu(); + } + }; + if (!isNew) menu[/*LANG*/"Delete"] = function() { + E.showPrompt("Delete Button?").then(function(yes) { + if (yes) { + settings.buttons.splice(settings.buttons.indexOf(button),1); + saveSettings(); + } + showMenu(); + }); + } + E.showMenu(menu); + } + + function showMenu() { + var menu = { "": {title:"BTHome", back:back}, + /*LANG*/"Show Battery" : { + value : !!settings.showBattery, + onchange : v=>{ + settings.showBattery = v; + saveSettings(); + } + } + }; + settings.buttons.forEach((button,idx) => { + var img = require("icons").getIcon(button.icon); + menu[/*LANG*/"Button"+(img ? " \0"+img : (idx+1))] = function() { + showButtonMenu(button, false); + }; + }); + menu[/*LANG*/"Add Button"] = function() { + showButtonMenu(undefined, true); + }; + E.showMenu(menu); + } + showMenu(); +}) \ No newline at end of file