diff --git a/README.md b/README.md
index 9571bef..b5d6d4e 100644
--- a/README.md
+++ b/README.md
@@ -35,8 +35,8 @@ service that does not need any more information but the entity id.
# Configuration
There are two sections on the plugin's cofiguration panel:
* Home Assistant Settings
- Contains global settings for your Home Assistant installation. Once saved, the settings are valid for all Buttons.
- * Entity Settings
+ Contains global settings for your Home Assistant installation. Once saved, the settings are used for all stream deck Buttons.
+ * Entity Settings
Contains settings for an individual button.
## Home Assistant Settings
@@ -52,7 +52,8 @@ After you saved your Home Assistant Settings, the plugin will automatically try
### Basic configuration
* Domain: Home Assistant entities are grouped by domains. Select the domain (for example "switch") of an entity, you want to configure.
* Entity: This is the actual entity you are going to configure (for example "Kitchen Light")
- * Service: The service that will be called every time you press the StreamDeck button for this entity.
+ * Service: The service that will be called every time you press the StreamDeck button.
+ * Service (long press): The service that will be called every time you press and hold the StreamDeck button for more than 300ms.
* Service Data JSON: JSON formatted data that is sent with your service call when you press a button.
Example:
```
@@ -79,25 +80,3 @@ After you hit the save button, the button should immediately show the new config
# Happy? Consider to donating me a coffee :)
[![buy me a coffee](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/donate?hosted_button_id=3UKRJEJVWV9H4)
-## Project setup
-```
-npm install
-```
-
-### Compiles and hot-reloads for development
-```
-npm run serve
-```
-
-### Compiles and minifies for production
-```
-npm run build
-```
-
-### Lints and fixes files
-```
-npm run lint
-```
-
-### Customize configuration
-See [Configuration Reference](https://cli.vuejs.org/config/).
diff --git a/doc/entity_settings.png b/doc/entity_settings.png
index ed9e918..4743e63 100644
Binary files a/doc/entity_settings.png and b/doc/entity_settings.png differ
diff --git a/doc/example.png b/doc/example.png
index 2570185..f61b7e8 100644
Binary files a/doc/example.png and b/doc/example.png differ
diff --git a/doc/ha_settings.png b/doc/ha_settings.png
index e2f6410..2e557b8 100644
Binary files a/doc/ha_settings.png and b/doc/ha_settings.png differ
diff --git a/package-lock.json b/package-lock.json
index f610f2f..61128ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1103,6 +1103,61 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==",
"dev": true
},
+ "@nuxt/opencollective": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.3.2.tgz",
+ "integrity": "sha512-XG7rUdXG9fcafu9KTDIYjJSkRO38EwjlKYIb5TQ/0WDbiTUTtUtgncMscKOYzfsY86kGs05pAuMOR+3Fi0aN3A==",
+ "requires": {
+ "chalk": "^4.1.0",
+ "consola": "^2.15.0",
+ "node-fetch": "^2.6.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"@soda/friendly-errors-webpack-plugin": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz",
@@ -2692,6 +2747,28 @@
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=",
"dev": true
},
+ "bootstrap": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
+ "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw=="
+ },
+ "bootstrap-vue": {
+ "version": "2.21.2",
+ "resolved": "https://registry.npmjs.org/bootstrap-vue/-/bootstrap-vue-2.21.2.tgz",
+ "integrity": "sha512-0Exe+4MZysqhZNXIKf4TzkvXaupxh9EHsoCRez0o5Dc0J7rlafayOEwql63qXv74CgZO8E4U8ugRNJko1vMvNw==",
+ "requires": {
+ "@nuxt/opencollective": "^0.3.2",
+ "bootstrap": ">=4.5.3 <5.0.0",
+ "popper.js": "^1.16.1",
+ "portal-vue": "^2.1.7",
+ "vue-functional-data-merge": "^3.1.0"
+ }
+ },
+ "bootswatch": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/bootswatch/-/bootswatch-4.6.0.tgz",
+ "integrity": "sha512-Yr6YqFBC8jwTzoJoLViYlvO97IhPWGqZEm+6FXHfD/G6gbUok6sZkdXxdh4Zb6iCGEwr9p7zGCn38yKQD/bh2Q=="
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -3470,6 +3547,11 @@
"integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==",
"dev": true
},
+ "consola": {
+ "version": "2.15.3",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
+ "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw=="
+ },
"console-browserify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -7476,6 +7558,11 @@
"lower-case": "^1.1.1"
}
},
+ "node-fetch": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
+ },
"node-forge": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz",
@@ -8137,6 +8224,16 @@
"ts-pnp": "^1.1.6"
}
},
+ "popper.js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
+ },
+ "portal-vue": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz",
+ "integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g=="
+ },
"portfinder": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
@@ -10990,6 +11087,11 @@
}
}
},
+ "vue-functional-data-merge": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
+ "integrity": "sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA=="
+ },
"vue-hot-reload-api": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
diff --git a/package.json b/package.json
index df5795e..c535c0e 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
+ "bootstrap": "^4.6.0",
+ "bootstrap-vue": "^2.21.2",
+ "bootswatch": "^4.6.0",
"core-js": "^3.6.5",
"nunjucks": "^3.2.3",
"vue": "^2.6.11"
diff --git a/public/pi.html b/public/pi.html
index 2040990..db1f41f 100644
--- a/public/pi.html
+++ b/public/pi.html
@@ -6,7 +6,6 @@
name=viewport>
-
HomeAssistant Plugin PI
diff --git a/src/Pi.vue b/src/Pi.vue
index 8f906c9..84fbf00 100644
--- a/src/Pi.vue
+++ b/src/Pi.vue
@@ -1,103 +1,184 @@
-
-
-
-
Home Assistant Settings
-
-
-
-
-
-
-
-
-
-
-
-
Entity Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
Settings should open in a separate window!
+
+
+
+
+
+ Home Assistant Settings
+
+
+
+
+
- Available variables: {{ entityAttributes }}
- You have to clear the main title to make this title template work.
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
{{haError}}
+
+
+ Save
+
-
-
-
+
+
+
+
+
+ Entity Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Clear
+
+
+
+
+
+
+
+
+
+
+
+
+ Clear
+
+
+
+
+
+
+
+
+ Enable custom button title
+
+
+
+
You have to clear the main title in the main stream deck window to make this title
+ template work.
+
+
+
- Available variables: {{ entityAttributes }}
-
-
-
+
+ Enable custom labels
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
-
+
@@ -105,9 +186,11 @@
import StreamDeck from "@/modules/common/streamdeck";
import {ObjectUtils} from "@/modules/common/utils";
import {Entity, Homeassistant} from "@/modules/common/homeassistant";
+import WindowPortal from "@/components/WindowPortal";
export default {
name: 'Pi',
+ components: {WindowPortal},
props: {},
data: () => {
return {
@@ -116,8 +199,11 @@ export default {
domain: "",
entity: "",
+
service: "",
serviceData: "",
+ serviceLongPress: "",
+ serviceDataLongPress: "",
// Custom Labels
useCustomTitle: false,
@@ -131,7 +217,11 @@ export default {
availableDomains: [],
availableEntities: [],
availableServices: [],
- availableAttributes: []
+ availableAttributes: [],
+
+ // Home-Assistant-State
+ haConnected: false,
+ haError: ""
}
},
created() {
@@ -153,8 +243,15 @@ export default {
let actionSettings = actionInfo.payload.settings
this.domain = actionSettings["domain"]
this.entity = actionSettings["entityId"]
- this.service = actionSettings["service"]
- this.serviceData = actionSettings["serviceData"]
+
+ if (actionSettings["service"]) {
+ this.service = actionSettings["service"].id
+ this.serviceData = actionSettings["service"].data
+ }
+ if (actionSettings["serviceLongPress"]) {
+ this.serviceLongPress = actionSettings["serviceLongPress"].id
+ this.serviceDataLongPress = actionSettings["serviceLongPress"].data
+ }
this.useCustomTitle = actionSettings["useCustomTitle"]
this.buttonTitle = actionSettings["buttonTitle"] || "{{friendly_name}}"
@@ -168,6 +265,38 @@ export default {
},
computed: {
+ serverUrlState: function () {
+ return this.serverUrl.length > 4
+ },
+
+ accessTokenState: function () {
+ return this.accessToken.length > 4
+ },
+
+ serviceDataFeedback: function () {
+ if (!this.serviceData) {
+ return "";
+ }
+ try {
+ const json = JSON.parse(this.serviceData);
+ return (typeof json === "object") ? "" : "Service data must be an JSON object."
+ } catch (e) {
+ return "Invalid JSON string.";
+ }
+ },
+
+ serviceDataLongPressFeedback: function () {
+ if (!this.serviceDataLongPress) {
+ return "";
+ }
+ try {
+ const json = JSON.parse(this.serviceDataLongPress);
+ return (typeof json === "object") ? "" : "Service data must be an JSON object."
+ } catch (e) {
+ return "Invalid JSON string.";
+ }
+ },
+
isHaSettingsComplete: function () {
return !this.serverUrl || !this.accessToken
},
@@ -205,39 +334,50 @@ export default {
}
this.$HA = new Homeassistant(this.serverUrl, this.accessToken, () => {
- this.$HA.getStates((states) => {
- this.availableDomains = states
- .map(state => new Entity(state.entity_id).domain)
- .sort()
- .reduce(
- (acc, curr) => acc.add(curr), new Set()
- );
-
- this.availableEntities = states
- .map((state) => {
+ this.haConnected = true;
+ this.$HA.getStates((states) => {
+ this.availableDomains = Array.from(states
+ .map(state => new Entity(state.entity_id).domain)
+ .sort()
+ .reduce(
+ (acc, curr) => acc.add(curr), new Set()
+ ));
+
+ this.availableEntities = states
+ .map((state) => {
+ return {
+ value: new Entity(state.entity_id),
+ text: state.attributes.friendly_name || state.entity_id
+ }
+ }
+ )
+ .sort((a, b) => (a.text > b.text) ? 1 : ((b.text > a.text) ? -1 : 0))
+
+ this.availableAttributes = states
+ .map((state) => {
return {
- value: new Entity(state.entity_id),
- text: state.attributes.friendly_name || state.entity_id
+ entity: new Entity(state.entity_id),
+ attributes: ObjectUtils.paths(state.attributes)
}
- }
- )
- .sort((a, b) => (a.text > b.text) ? 1 : ((b.text > a.text) ? -1 : 0))
-
- this.availableAttributes = states
- .map((state) => {
- return {
- entity: new Entity(state.entity_id),
- attributes: ObjectUtils.paths(state.attributes)
- }
- })
- });
- this.$HA.getServices((services) => {
- this.availableServices = services;
- });
- })
+ })
+ });
+ this.$HA.getServices((services) => {
+ this.availableServices = services;
+ });
+ },
+ () => {
+ this.haConnected = false;
+ this.haError = "Failed to connect websocket.";
+ },
+ () => {
+ this.haConnected = false;
+ this.haError = "Websocket was closed.";
+ }
+ )
},
saveGlobalSettings: function () {
+ this.haError = "";
this.$SD.saveGlobalSettings({"serverUrl": this.serverUrl, "accessToken": this.accessToken});
this.connectHomeAssistant()
},
@@ -246,8 +386,15 @@ export default {
let actionSettings = {
domain: this.domain,
entityId: this.entity,
- service: this.service,
- serviceData: this.serviceData,
+
+ service: {
+ id: this.service,
+ data: this.serviceData
+ },
+ serviceLongPress: {
+ id: this.serviceLongPress,
+ data: this.serviceDataLongPress
+ },
useCustomTitle: this.useCustomTitle,
buttonTitle: this.buttonTitle,
diff --git a/src/Plugin.vue b/src/Plugin.vue
index 4de55f8..528f88f 100644
--- a/src/Plugin.vue
+++ b/src/Plugin.vue
@@ -17,7 +17,8 @@ export default {
$HA: null,
$reconnectTimeout: null,
actionSettings: {},
- globalSettings: {}
+ globalSettings: {},
+ buttonLongpressTimeouts: new Map() //context, timeout
}
},
beforeCreate() {
@@ -64,24 +65,21 @@ export default {
})
this.$SD.on("keyDown", (message) => {
- console.log(this.actionSettings)
- if (this.$HA) {
- let context = message.context
- let settings = this.actionSettings[context];
- if (settings.service) {
- try {
- const entity = new Entity(settings.entityId);
- const serviceData = settings.serviceData ? JSON.parse(settings.serviceData) : {};
- // add default entity_id if not specified
- if (!serviceData.entity_id) {
- serviceData.entity_id = entity.entityId;
- }
- this.$HA.callService(settings.service, entity, serviceData)
- } catch (e) {
- console.error(e)
- this.$SD.showAlert(context);
- }
- }
+ let context = message.context
+
+ const timeout = setTimeout(buttonLongPress, 300, context);
+ this.buttonLongpressTimeouts.set(context, timeout)
+ })
+
+ this.$SD.on("keyUp", (message) => {
+ let context = message.context
+
+ // If "long press timeout" is still present, we perform a normal press
+ const lpTimeout = this.buttonLongpressTimeouts.get(context);
+ if (lpTimeout) {
+ clearTimeout(lpTimeout);
+ this.buttonLongpressTimeouts.delete(context)
+ buttonShortPress(context);
}
})
@@ -107,6 +105,41 @@ export default {
}
})
+ const buttonShortPress = (context) => {
+ let settings = this.actionSettings[context];
+ callService(context, settings.service);
+ }
+
+ const buttonLongPress = (context) => {
+ this.buttonLongpressTimeouts.delete(context);
+ let settings = this.actionSettings[context];
+ if (settings.serviceLongPress.id) {
+ callService(context, settings.serviceLongPress);
+ } else {
+ callService(context, settings.service);
+ }
+ }
+
+ const callService = (context, serviceToCall) => {
+ let settings = this.actionSettings[context];
+ if (this.$HA) {
+ if (serviceToCall) {
+ try {
+ const entity = new Entity(settings.entityId);
+ const serviceData = serviceToCall.data ? JSON.parse(serviceToCall.data) : {};
+ // add default entity_id if not specified
+ if (!serviceData.entity_id) {
+ serviceData.entity_id = entity.entityId;
+ }
+ this.$HA.callService(serviceToCall.id, entity, serviceData)
+ } catch (e) {
+ console.error(e)
+ this.$SD.showAlert(context);
+ }
+ }
+ }
+ }
+
const connectHomeAssistant = () => {
if (this.globalSettings.serverUrl && this.globalSettings.accessToken) {
if (this.$HA) {
diff --git a/src/components/WindowPortal.vue b/src/components/WindowPortal.vue
new file mode 100644
index 0000000..92e2b6e
--- /dev/null
+++ b/src/components/WindowPortal.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/src/modules/common/streamdeck.js b/src/modules/common/streamdeck.js
index 6824ceb..b7dcddf 100644
--- a/src/modules/common/streamdeck.js
+++ b/src/modules/common/streamdeck.js
@@ -27,6 +27,9 @@ export default class StreamDeck {
case "keyDown":
this.events.emit("keyDown", incomingEvent)
break;
+ case "keyUp":
+ this.events.emit("keyUp", incomingEvent)
+ break;
case "willAppear":
this.events.emit("willAppear", incomingEvent)
break;
diff --git a/src/modules/common/utils.js b/src/modules/common/utils.js
index 23af2c3..3674ae2 100644
--- a/src/modules/common/utils.js
+++ b/src/modules/common/utils.js
@@ -16,7 +16,7 @@ export const ObjectUtils = {
.map(subPath => `${keyPath}.${subPath}`)
.forEach(path => paths.push(path))
} else {
- paths.push(key)
+ paths.push(keyPath)
}
}
)
diff --git a/src/pi/main.js b/src/pi/main.js
index 84d111d..d076089 100644
--- a/src/pi/main.js
+++ b/src/pi/main.js
@@ -1,5 +1,15 @@
import Vue from 'vue'
import Pi from "@/Pi";
+import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
+
+import 'bootstrap/dist/css/bootstrap.css'
+import 'bootstrap-vue/dist/bootstrap-vue.css'
+import "bootswatch/dist/superhero/bootstrap.min.css";
+
+// Make BootstrapVue available throughout your project
+Vue.use(BootstrapVue)
+// Optionally install the BootstrapVue icon components plugin
+Vue.use(IconsPlugin)
Vue.config.productionTip = false