Merge pull request #3375 from bobrippling/feat/ha

ha: add value triggers (using the Slider lib)
master
Rob Pilling 2024-04-22 20:31:57 +01:00 committed by GitHub
commit 86680eb1d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 143 additions and 56 deletions

View File

@ -9,3 +9,4 @@
0.09: Improve web interface, arrows in UI
0.10: Issue newline before GB commands (solves issue with console.log and ignored commands)
0.11: Minor code improvements
0.12: Add slider functionality

View File

@ -3,11 +3,13 @@ This app integrates your Bangle.js into the Home Assistant.
# How to use
Click on the left or right side of the screen to select the triggers that you configured.
Click on the left or right side of the screen to select the triggers that you configured.
Swiping left or right works as well.
Click in the middle of the screen to send the trigger to Home Assistant via Gadgetbridge.
If the trigger is a value one (has `.value = true` in the config), a slider will be displayed for you to alter the value that's sent along with the trigger.
![](screenshot.png)

View File

@ -5,7 +5,7 @@ var ha = require("ha.lib.js");
var W = g.getWidth(), H = g.getHeight();
var position=0;
var triggers = ha.getTriggers();
var slider;
function draw() {
g.reset().clearRect(Bangle.appRect);
@ -17,15 +17,60 @@ function draw() {
g.setFontAlign(-1,-1);
var icon = trigger.getIcon();
g.setColor(g.theme.fg).drawImage(icon, 12, H/5-2-5);
g.drawString("Home", icon.width + 20, H/5-5);
g.drawString("Assistant", icon.width + 18, H/5+24-5);
var iconY = H / 5 - 2 - 5;
g.setColor(g.theme.fg).drawImage(icon, 12, iconY);
g.setFontAlign(0,0);
var ypos = H/5*3+23;
g.drawRect(W/2-w/2-8, ypos-h/2-8, W/2+w/2+5, ypos+h/2+5);
g.fillRect(W/2-w/2-6, ypos-h/2-6, W/2+w/2+3, ypos+h/2+3);
g.setColor(g.theme.bg).drawString(trigger.display, W/2, ypos);
if (trigger.value) {
if (!slider) {
const R = Bangle.appRect;
console.log("R", R);
const w = 50;
slider = require("Slider").create(onSlide, {
initLevel: 0, // TODO: fetch this?
mode: "map",
steps: 100,
timeout: false,
width: w,
xStart: R.w / 2 - w / 2,
yStart: R.y,
height: R.h - 24,
dragRect: {
x: R.w * 0.3,
x2: R.w * 0.6,
y: R.y,
y2: R.h,
},
});
Bangle.prependListener('drag', slider.f.dragSlider);
}
const r = slider.c.borderRect;
g.setColor(g.theme.fg)
.setFontAlign(0, 0)
.drawString("HA", (r.x + r.w + W) / 2, iconY + g.imageMetrics(icon).height / 2)
.setFontAlign(0, 1)
.drawString(trigger.display, W / 2, H);
slider.f.draw(slider.v.level);
}else{
if (slider) {
slider.f.remove();
slider = undefined;
}
g.drawString("Home", icon.width + 20, H/5-5);
g.drawString("Assistant", icon.width + 18, H/5+24-5);
g.setFontAlign(0,0);
var ypos = H/5*3+23;
g.drawRect(W/2-w/2-8, ypos-h/2-8, W/2+w/2+5, ypos+h/2+5);
g.fillRect(W/2-w/2-6, ypos-h/2-6, W/2+w/2+3, ypos+h/2+3);
g.setColor(g.theme.bg).drawString(trigger.display, W/2, ypos);
}
// draw arrows
g.setColor(g.theme.fg);
@ -39,6 +84,26 @@ function draw() {
}
}
var lastLevel;
var lastTouch;
function onSlide(mode, level, e) {
lastTouch = Date.now();
if (!e) return;
if (e.b !== 0) {
if (lastLevel == null)
lastLevel = level;
} else {
if (lastLevel != null && lastLevel !== level) {
// we've had a drag and level has changed
ha.sendValue(triggers[position].trigger, level);
lastLevel = null;
}
}
}
function toLeft() {
Bangle.buzz(40, 0.6);
position -= 1;
@ -63,31 +128,36 @@ function sendTrigger() {
});
}
Bangle.on('touch', function(btn, e){
var left = parseInt(g.getWidth() * 0.3);
Bangle.on('touch', (btn, e) => {
if (Date.now() - lastTouch < 250) return;
lastTouch = Date.now();
var left = g.getWidth() * 0.3;
var right = g.getWidth() - left;
var isLeft = e.x < left;
var isRight = e.x > right;
if(isLeft){
toLeft();
}
if(isRight){
}else if (isRight){
toRight();
}
if(!isRight && !isLeft){
}else{
sendTrigger();
}
});
Bangle.on("swipe", (lr,ud) => {
if (lr == -1) {
toLeft();
}
if (lr == 1) {
toRight();
}
});
if (slider) return; // "disable" swiping on sliders
if (Date.now() - lastTouch < 250) return;
lastTouch = Date.now();
if (lr == -1) {
toLeft();
} else if (lr == 1) {
toRight();
}
E.stopEventPropagation && E.stopEventPropagation();
});
// Send intent that the we started the app.

View File

@ -2,8 +2,8 @@
* This library can be used to read all triggers that a user
* configured and send a trigger to homeassistant.
*/
function _getIcon(trigger){
const icon = trigger.icon;
function _getIcon(){
const icon = this.icon;
if(icon == "light"){
return {
width : 48, height : 48, bpp : 1,
@ -33,28 +33,27 @@ function _getIcon(trigger){
}
exports.getTriggers = function(){
var triggers = [
{display: "Empty", trigger: "NOP", icon: "ha"},
];
var triggers;
try{
triggers = require("Storage").read("ha.trigger.json");
triggers = JSON.parse(triggers);
// We lazy load all icons, otherwise, we have to keep
// all the icons n times in memory which can be
// problematic for embedded devices. Therefore,
// we lazy load icons only if needed using the getIcon
// method of each trigger...
triggers.forEach(trigger => {
trigger.getIcon = function(){
return _getIcon(trigger);
}
})
triggers = require("Storage").readJSON("ha.trigger.json");
} catch(e) {
// In case there are no user triggers yet, we show the default...
console.log("ha: error loading triggers:", e);
triggers = [
{display: "Empty", trigger: "NOP", icon: "ha"},
];
}
// We lazy load all icons, otherwise, we have to keep
// all the icons n times in memory which can be
// problematic for embedded devices. Therefore,
// we lazy load icons only if needed using the getIcon
// method of each trigger...
triggers.forEach(trigger => {
trigger.getIcon = _getIcon.bind(trigger);
})
return triggers;
}
@ -66,16 +65,26 @@ exports.sendTrigger = function(triggerName){
// Now lets send the trigger that we sould send.
Bluetooth.println("");
Bluetooth.println(JSON.stringify({
t:"intent",
action:"com.espruino.gadgetbridge.banglejs.HA",
extra:{
trigger: triggerName
}})
t:"intent",
action:"com.espruino.gadgetbridge.banglejs.HA",
extra:{
trigger: triggerName
}})
);
retries = -1;
break;
} catch(e){
retries--;
}
}
}
}
exports.sendValue = function(trigger, value){
Bluetooth.println(
JSON.stringify({
t: "intent",
action: "com.espruino.gadgetbridge.banglejs.HA",
extra: { trigger, value },
})
);
};

View File

@ -1,7 +1,7 @@
{
"id": "ha",
"name": "Home Assistant",
"version": "0.11",
"version": "0.12",
"description": "Integrates your Bangle.js into Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png",
"type": "app",

View File

@ -125,6 +125,7 @@ exports.create = function(cb, conf) {
if (o.v.timeoutID) {clearTimeout(o.v.timeoutID); o.v.timeoutID = undefined;}
if (e.b==0 && !o.v.timeoutID && (o.c.timeout || o.c.timeout===0)) o.v.timeoutID = setTimeout(o.f.remove, 1000*o.c.timeout);
let cbObj;
if (useMap && o.f.wasOnIndicator(o.v.exFirst)) { // If draging starts on the indicator, adjust one-to-one.
let input = !o.c.horizontal?
@ -134,7 +135,7 @@ exports.create = function(cb, conf) {
o.v.level = Math.min(Math.max(input,0),o.c.steps);
o.v.cbObj = {mode:"map", value:o.v.level};
cbObj = {mode:"map", value:o.v.level};
} else if (useIncr) { // Heavily inspired by "updown" mode of setUI.
@ -149,16 +150,16 @@ exports.create = function(cb, conf) {
o.v.level = Math.min(Math.max(o.v.level-incr,0),o.c.steps);
o.v.cbObj = {mode:"incr", value:incr};
cbObj = {mode:"incr", value:incr};
}
}
if (o.v.cbObj && (o.v.level!==o.v.prevLevel||o.v.level===0||o.v.level===o.c.steps)) {
cb(o.v.cbObj.mode, o.v.cbObj.value);
if (cbObj && (o.v.level!==o.v.prevLevel||o.v.level===0||o.v.level===o.c.steps||e.b===0)) {
cb(cbObj.mode, cbObj.value, e);
o.f.draw&&o.f.draw(o.v.level);
}
o.v.cbObj = null;
o.v.prevLevel = o.v.level;
o.v.ebLast = e.b;
if (e.b==0) o.v.dragActive = false;
}
};

View File

@ -18,12 +18,16 @@ Bangle.on("drag", slider.f.dragSlider);
// Bangle.prependListener("drag", slider.f.dragSlider);
```
`callbackFunction` (`cb`) (first argument) determines what `slider` is used for. `slider` will pass two arguments, `mode` and `feedback` (`fb`), into `callbackFunction` (if `slider` is interactive or auto progressing). The different `mode`/`feedback` combinations to expect are:
`callbackFunction` (`cb`) (first argument) determines what `slider` is used for. `slider` will pass three arguments, `mode`, `feedback` (`fb`) and (if fired from an input event) `event` (`e`), into `callbackFunction` (if `slider` is interactive or auto progressing). The different `mode`/`feedback` combinations to expect are:
- `"map", o.v.level` | current level when interacting by mapping interface.
- `"incr", incr` | where `incr` == +/-1, when interacting by incrementing interface.
- `"remove", o.v.level` | last level when the slider times out.
- `"auto", o.v.level` | when auto progressing.
The event will be a drag, from the `Bangle.on('drag', ...)` event.
The callback function will always be called for the "final" event, which is when the user lifts their finger from the screen. This can be detected by looking for `e.b == 0`.
`configObject` (`conf`) (second argument, optional) has the following defaults:
```js
@ -91,7 +95,7 @@ slider = require("Slider").create(()=>{}, {autoProgress:true})
r: 22 }
}
}
>
>
```
Tips
----