Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streamdeck homeassistant 117 #182

Merged
merged 4 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
}
],
"SupportedInMultiActions": true,
"UUID": "de.perdoctus.streamdeck.homeassistant.entity"
"UUID": "de.perdoctus.streamdeck.homeassistant.entity",
"Controllers": ["Keypad", "Encoder"],
"Encoder": {
"layout": "$A0",
"StackColor": "#AABBCC"
}
},

{
Expand Down
85 changes: 67 additions & 18 deletions src/components/PiComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
</div>

<div v-if="haConnectionState === 'connected'" class="clearfix mb-3">
<h1>Button appearance</h1>
<h1>{{ controllerType }} appearance</h1>

<div class="mb-3">
<label class="form-label" for="accessToken">Domain</label>
Expand Down Expand Up @@ -115,19 +115,54 @@
<label class="form-check-label" for="chkHideIcon">Hide icon</label>
</div>

<h1>Button actions</h1>

<h6>Short Press</h6>
<ServiceCallConfiguration v-model="serviceShortPress"
:available-entities="availableEntities"
:available-services="availableServices"

></ServiceCallConfiguration>

<h6>Long Press</h6>
<ServiceCallConfiguration v-model="serviceLongPress"
:available-entities="availableEntities"
:available-services="availableServices"></ServiceCallConfiguration>
<h1>{{ controllerType }} actions</h1>

<AccordeonComponent id="presses" class="mb-2">
<AccordeonItem accordeon-id="presses" item-id="shortPress" title="Short Press">
<ServiceCallConfiguration v-model="serviceShortPress" :available-entities="availableEntities"
:available-services="availableServices"
class="mb-2"

></ServiceCallConfiguration>
</AccordeonItem>

<AccordeonItem accordeon-id="presses" item-id="longPress" title="Long Press">
<ServiceCallConfiguration v-model="serviceLongPress" :available-entities="availableEntities"
:available-services="availableServices"
class="mb-2"></ServiceCallConfiguration>

</AccordeonItem>

<template v-if="controllerType === 'Encoder'">
<AccordeonItem accordeon-id="presses" item-id="touch" title="Screen tap">
<ServiceCallConfiguration v-model="serviceTap" :available-entities="availableEntities"
:available-services="availableServices"
class="mb-2"></ServiceCallConfiguration>
</AccordeonItem>

<AccordeonItem accordeon-id="presses" item-id="dialRotate" title="Rotation">
<ServiceCallConfiguration v-model="serviceRotation"
:available-entities="availableEntities"
:available-services="availableServices"></ServiceCallConfiguration>
<details class="mb-2">
<summary>Available variables</summary>
<div class="form-text">
<span v-pre class="text-info font-monospace">{{ ticks }}</span> - The number of ticks the dial was
rotated (negative value for left turn, positive value for right turn).
</div>
<div class="form-text">
<span v-pre class="text-info font-monospace">{{ rotationPercent }}</span> - A number between 0 and 100
that represents the rotation percentage value of the dial.
</div>
<div class="form-text">
<span v-pre class="text-info font-monospace">{{ rotationAbsolute }}</span> - A number between 0 and 255
that represents the absolute rotation value of the dial.
</div>
</details>
</AccordeonItem>
</template>

</AccordeonComponent>

<button id="mybutton" :disabled="!domain" class="btn btn-sm btn-primary float-end"
v-on:click="saveSettings">
Expand All @@ -148,6 +183,8 @@ import {Service} from "@/modules/pi/service";
import {computed, onMounted, ref} from "vue";
import ServiceCallConfiguration from "@/components/ServiceCallConfiguration.vue";
import {ObjectUtils} from "@/modules/common/utils";
import AccordeonComponent from "@/components/accordeon/BootstrapAccordeon.vue";
import AccordeonItem from "@/components/accordeon/BootstrapAccordeonItem.vue";

let $HA = null;
let $SD = null;
Expand All @@ -156,8 +193,12 @@ const serverUrl = ref("")
const accessToken = ref("")
const domain = ref("")
const entity = ref("")

const serviceShortPress = ref({})
const serviceLongPress = ref({})
const serviceTap = ref({})
const serviceRotation = ref({})

const useCustomTitle = ref(false)
const buttonTitle = ref("{{friendly_name}}")
const useStateImagesForOnOffStates = ref(false) // determined by action ID (manifest)
Expand All @@ -173,16 +214,18 @@ const currentStates = ref([])
const haConnectionState = ref("disconnected") // disconnected, connecting, connected
const haError = ref("")

const controllerType = ref("")


onMounted(() => {
window.connectElgatoStreamDeckSocket = (inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo) => {
$SD = new StreamDeck(inPort, inPropertyInspectorUUID, inRegisterEvent, inInfo, inActionInfo);

// Dual State entity (custom icons for on/off)
const inActionInfoObject = JSON.parse(inActionInfo);
if (inActionInfoObject["action"] === "de.perdoctus.streamdeck.homeassistant.dual-state-entity") {
useStateImagesForOnOffStates.value = true;
}

useStateImagesForOnOffStates.value = inActionInfoObject["action"] === "de.perdoctus.streamdeck.homeassistant.dual-state-entity";
controllerType.value = inActionInfoObject.payload.controller

$SD.on("globalsettings", (globalSettings) => {
if (globalSettings) {
Expand Down Expand Up @@ -210,6 +253,8 @@ onMounted(() => {
buttonLabels.value = settings["display"]["buttonLabels"]
serviceShortPress.value = settings["button"]["serviceShortPress"]
serviceLongPress.value = settings["button"]["serviceLongPress"]
serviceTap.value = settings["button"]["serviceTap"]
serviceRotation.value = settings["button"]["serviceRotation"]
})
}
})
Expand Down Expand Up @@ -301,7 +346,9 @@ function saveGlobalSettings() {

function saveSettings() {
let settings = {
version: 3,
version: 4,

controllerType: controllerType.value,

display: {
domain: domain.value,
Expand All @@ -318,6 +365,8 @@ function saveSettings() {
button: {
serviceShortPress: serviceShortPress.value,
serviceLongPress: serviceLongPress.value,
serviceTap: serviceTap.value,
serviceRotation: serviceRotation.value
}

}
Expand Down
110 changes: 86 additions & 24 deletions src/components/PluginComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@
<script setup>
import {StreamDeck} from "@/modules/common/streamdeck";
import {Homeassistant} from "@/modules/homeassistant/homeassistant";
import {EntityButtonImageFactory, EntityConfigFactory} from "@/modules/plugin/entityButtonImageFactory";
import {EntityButtonImageFactory} from "@/modules/plugin/entityButtonImageFactory";
import nunjucks from "nunjucks"
import {Settings} from "@/modules/common/settings";
import {onMounted, ref} from "vue";
import {EntityConfigFactory} from "@/modules/plugin/entityConfigFactory";


const entityConfigFactory = new EntityConfigFactory()
const buttonImageFactory = new EntityButtonImageFactory()
const touchScreenImageFactory = new EntityButtonImageFactory({width: 200, height: 100})

const $SD = ref(null)
const $HA = ref(null)
const $reconnectTimeout = ref({})
// const useStateImagesForOnOffStates = ref(false)
const actionSettings = ref({})
const actionSettings = ref([])
const buttonLongpressTimeouts = ref(new Map()) //context, timeout

let rotationTimeout = [];
let rotationAmount = [];
let rotationPercent = [];

onMounted(() => {
window.connectElgatoStreamDeckSocket = (inPort, inPluginUUID, inRegisterEvent, inInfo) => {
$SD.value = new StreamDeck(inPort, inPluginUUID, inRegisterEvent, inInfo, "{}");
Expand All @@ -32,7 +37,6 @@ onMounted(() => {
}
)


const onHAConnected = () => {
$HA.value.getStates(entitiyStatesChanged)
$HA.value.subscribeEvents(entityStateChanged)
Expand Down Expand Up @@ -61,26 +65,17 @@ onMounted(() => {
})

$SD.value.on("keyDown", (message) => {
let context = message.context

const timeout = setTimeout(buttonLongPress, 300, context);
buttonLongpressTimeouts.value.set(context, timeout)
buttonDown(message.context);
})

$SD.value.on("keyUp", (message) => {
let context = message.context

// If "long press timeout" is still present, we perform a normal press
const lpTimeout = buttonLongpressTimeouts.value.get(context);
if (lpTimeout) {
clearTimeout(lpTimeout);
buttonLongpressTimeouts.value.delete(context)
buttonShortPress(context);
}
buttonUp(message.context);
})

$SD.value.on("willAppear", (message) => {
let context = message.context;
rotationAmount[context] = 0;
rotationPercent[context] = 0;
actionSettings.value[context] = Settings.parse(message.payload.settings)
if ($HA.value) {
$HA.value.getStates(entitiyStatesChanged)
Expand All @@ -92,36 +87,94 @@ onMounted(() => {
delete actionSettings.value[context]
})

$SD.value.on("dialRotate", (message) => {
let context = message.context;
let settings = actionSettings.value[context];

rotationAmount[context] += message.payload.ticks;
rotationPercent[context] += (message.payload.ticks * 2);
if (rotationPercent[context] < 0) {
rotationPercent[context] = 0;
} else if (rotationPercent[context] > 100) {
rotationPercent[context] = 100;
}

if (rotationTimeout[context])
return;

rotationTimeout[context] = setTimeout(() => {
callService(context, settings.button.serviceRotation, {ticks: rotationAmount[context], rotationPercent: rotationPercent[context], rotationAbsolute: 2.55 * rotationPercent[context]});
rotationAmount[context] = 0;
rotationTimeout[context] = null;
}, 300);

})

$SD.value.on("dialDown", (message) => {
buttonDown(message.context);
})

$SD.value.on("dialUp", (message) => {
buttonUp(message.context);
})

$SD.value.on("touchTap", (message) => {
let context = message.context;
let settings = actionSettings.value[context];
callService(context, settings.button.serviceTap);
})

$SD.value.on("didReceiveSettings", (message) => {
let context = message.context;
rotationAmount[context] = 0;
actionSettings.value[context] = Settings.parse(message.payload.settings)

if ($HA.value) {
$HA.value.getStates(entitiyStatesChanged)
}
})

const buttonDown = (context) => {
const timeout = setTimeout(buttonLongPress, 300, context);
buttonLongpressTimeouts.value.set(context, timeout)
}

const buttonUp = (context) => {
// If "long press timeout" is still present, we perform a normal press
const lpTimeout = buttonLongpressTimeouts.value.get(context);
if (lpTimeout) {
clearTimeout(lpTimeout);
buttonLongpressTimeouts.value.delete(context)
buttonShortPress(context);
}
}

const buttonShortPress = (context) => {
let settings = actionSettings.value[context];
callService(context, settings.button.serviceShortPress);
}

const buttonLongPress = (context) => {
buttonLongpressTimeouts.value.delete(context);
let settings = actionSettings[context];
let settings = actionSettings.value[context];
if (settings.button.serviceLongPress.serviceId) {
callService(context, settings.button.serviceLongPress);
} else {
callService(context, settings.button.serviceShortPress);
}
}

const callService = (context, serviceToCall) => {
const callService = (context, serviceToCall, serviceDataAttributes = {}) => {
if ($HA.value) {
if (serviceToCall["serviceId"]) {
try {
const serviceIdParts = serviceToCall.serviceId.split('.');
const serviceData = serviceToCall.serviceData ? JSON.parse(serviceToCall.serviceData) : null;

let serviceData = null;
if (serviceToCall.serviceData) {
let renderedServiceData = nunjucks.renderString(serviceToCall.serviceData, serviceDataAttributes)
serviceData = JSON.parse(renderedServiceData);
}

$HA.value.callService(serviceIdParts[1], serviceIdParts[0], serviceToCall.entityId, serviceData)
} catch (e) {
console.error(e)
Expand Down Expand Up @@ -206,8 +259,13 @@ onMounted(() => {
$SD.value.setState(currentContext, 0);
}
} else {
const buttonImage = buttonImageFactory.createButton(entityConfig);
setButtonSVG(buttonImage, currentContext)
if (contextSettings.controllerType === 'Encoder') {
const buttonImage = touchScreenImageFactory.createButton(entityConfig);
setButtonSVG(buttonImage, currentContext)
} else {
const buttonImage = buttonImageFactory.createButton(entityConfig);
setButtonSVG(buttonImage, currentContext)
}
}

if (contextSettings.display.useCustomTitle) {
Expand All @@ -222,7 +280,11 @@ onMounted(() => {

const setButtonSVG = (svg, changedContext) => {
const image = "data:image/svg+xml;charset=utf8," + svg;
$SD.value.setImage(changedContext, image)
if (actionSettings.value[changedContext].controllerType === 'Encoder') {
$SD.value.setFeedback(changedContext, {"full-canvas": image, "canvas": null, "title": ""})
} else {
$SD.value.setImage(changedContext, image)
}
}
})

Expand Down
Loading
Loading