Skip to content

Commit

Permalink
Merge pull request #278 from cvpcs/encoderLayouts
Browse files Browse the repository at this point in the history
Initial encoder layout support
  • Loading branch information
cgiesche authored Jun 26, 2024
2 parents f8c7154 + 7045251 commit aeb3ce2
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 33 deletions.
58 changes: 35 additions & 23 deletions src/components/PiComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@

<EntitySelection class="mb-3" :available-entities="availableEntities" v-model="entity"></EntitySelection>

<template v-if="controllerType === 'Encoder'">
<div class="form-check">
<input id="chkUseEncoderLayout" v-model="useEncoderLayout" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkUseEncoderLayout">Use encoder layout</label>
</div>
</template>

<div class="form-check">
<input id="chkButtonTitle" v-model="useCustomTitle" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkButtonTitle">Use custom title</label>
Expand All @@ -67,32 +74,34 @@
</div>
</div>

<div class="form-check">
<input id="chkCustomLabels" v-model="useCustomButtonLabels" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkCustomLabels">Custom labels</label>
</div>
<template v-if="!useEncoderLayout">
<div class="form-check">
<input id="chkCustomLabels" v-model="useCustomButtonLabels" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkCustomLabels">Custom labels</label>
</div>

<div v-if="useCustomButtonLabels">
<div class="mb-3">
<textarea id="buttonLabels" v-model="buttonLabels" class="form-control font-monospace"
placeholder="Line 1 (may overlap with icon)"
rows="4"></textarea>
<details>
<summary>Available variables</summary>
<div v-for="attr in entityAttributes" v-bind:key="attr" class="form-text font-monospace">{{ attr }}</div>
</details>
<div v-if="useCustomButtonLabels">
<div class="mb-3">
<textarea id="buttonLabels" v-model="buttonLabels" class="form-control font-monospace"
placeholder="Line 1 (may overlap with icon)"
rows="4"></textarea>
<details>
<summary>Available variables</summary>
<div v-for="attr in entityAttributes" v-bind:key="attr" class="form-text font-monospace">{{ attr }}</div>
</details>
</div>
</div>
</div>

<div class="form-check">
<input id="chkEnableServiceIndicator" v-model="enableServiceIndicator" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkEnableServiceIndicator">Show visual service indicators</label>
</div>
<div class="form-check">
<input id="chkEnableServiceIndicator" v-model="enableServiceIndicator" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkEnableServiceIndicator">Show visual service indicators</label>
</div>

<div class="form-check mb-3">
<input id="chkHideIcon" v-model="hideIcon" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkHideIcon">Hide icon</label>
</div>
<div class="form-check mb-3">
<input id="chkHideIcon" v-model="hideIcon" class="form-check-input" type="checkbox">
<label class="form-check-label" for="chkHideIcon">Hide icon</label>
</div>
</template>

<h1>{{ controllerType }} actions</h1>

Expand Down Expand Up @@ -201,6 +210,7 @@ const rotationTickBucketSizeMs = ref(300)
const useCustomTitle = ref(false)
const buttonTitle = ref("{{friendly_name}}")
const useEncoderLayout = ref(false)
const useStateImagesForOnOffStates = ref(false) // determined by action ID (manifest)
const useCustomButtonLabels = ref(false)
const buttonLabels = ref("")
Expand Down Expand Up @@ -247,6 +257,7 @@ onMounted(() => {
hideIcon.value = settings["display"]["hideIcon"];
useCustomTitle.value = settings["display"]["useCustomTitle"]
buttonTitle.value = settings["display"]["buttonTitle"] || "{{friendly_name}}"
useEncoderLayout.value = settings["display"]["useEncoderLayout"]
useCustomButtonLabels.value = settings["display"]["useCustomButtonLabels"]
buttonLabels.value = settings["display"]["buttonLabels"]
serviceShortPress.value = settings["button"]["serviceShortPress"]
Expand Down Expand Up @@ -340,13 +351,14 @@ function saveGlobalSettings() {
function saveSettings() {
let settings = {
version: 4,
version: 5,
controllerType: controllerType.value,
display: {
entityId: entity.value,
useCustomTitle: useCustomTitle.value,
useEncoderLayout: useEncoderLayout.value,
buttonTitle: buttonTitle.value,
enableServiceIndicator: enableServiceIndicator.value,
hideIcon: hideIcon.value,
Expand Down
41 changes: 32 additions & 9 deletions src/components/PluginComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<script setup>
import {StreamDeck} from "@/modules/common/streamdeck";
import {Homeassistant} from "@/modules/homeassistant/homeassistant";
import {SvgUtils} from "@/modules/plugin/svgUtils";
import {EntityButtonImageFactory} from "@/modules/plugin/entityButtonImageFactory";
import nunjucks from "nunjucks"
import {Settings} from "@/modules/common/settings";
Expand All @@ -16,6 +17,7 @@ import axios from "axios";
const entityConfigFactory = new EntityConfigFactory()
const buttonImageFactory = new EntityButtonImageFactory()
const touchScreenImageFactory = new EntityButtonImageFactory({width: 200, height: 100})
const svgUtils = new SvgUtils();
const $SD = ref(null)
const $HA = ref(null)
Expand Down Expand Up @@ -220,7 +222,35 @@ function updateContextState(currentContext, domain, stateObject) {
entityConfig.isMultiAction = contextSettings.button.serviceLongPress.serviceId && (contextSettings.display.enableServiceIndicator === undefined || contextSettings.display.enableServiceIndicator) // undefined = on by default
entityConfig.hideIcon = contextSettings.display.hideIcon
if (contextSettings.display.useStateImagesForOnOffStates) {
if (entityConfig.rotationPercent !== undefined) {
rotationPercent[currentContext] = entityConfig.rotationPercent
}
if (contextSettings.display.useCustomTitle) {
let state = stateObject.state;
let stateAttributes = stateObject.attributes;
entityConfig.customTitle = nunjucks.renderString(contextSettings.display.buttonTitle, {...{state}, ...stateAttributes})
$SD.value.setTitle(currentContext, entityConfig.customTitle);
}
if (contextSettings.display.useEncoderLayout) {
if (!entityConfig.feedbackLayout) {
entityConfig.feedbackLayout = {layout: "$A1"};
}
$SD.value.setFeedbackLayout(currentContext, entityConfig.feedbackLayout);
if (!entityConfig.feedback) {
entityConfig.feedback = {}
}
entityConfig.feedback.title = entityConfig.customTitle !== undefined ? entityConfig.customTitle : "";
entityConfig.feedback.icon = "data:image/svg+xml;charset=utf8," + svgUtils.generateIconSVG(entityConfig.icon, entityConfig.color);
if (entityConfig.feedback.value === undefined) {
entityConfig.feedback.value = entityConfig.state;
}
$SD.value.setFeedback(currentContext, entityConfig.feedback);
} else if (contextSettings.display.useStateImagesForOnOffStates) {
if (activeStates.value.indexOf(stateObject.state) !== -1) {
console.log("Setting state of " + currentContext + " to 1")
$SD.value.setState(currentContext, 1);
Expand All @@ -237,19 +267,12 @@ function updateContextState(currentContext, domain, stateObject) {
setButtonSVG(buttonImage, currentContext)
}
}
if (contextSettings.display.useCustomTitle) {
let state = stateObject.state;
let stateAttributes = stateObject.attributes;
const customTitle = nunjucks.renderString(contextSettings.display.buttonTitle, {...{state}, ...stateAttributes})
$SD.value.setTitle(currentContext, customTitle);
}
}
function setButtonSVG(svg, changedContext) {
const image = "data:image/svg+xml;charset=utf8," + svg;
if (actionSettings.value[changedContext].controllerType === 'Encoder') {
$SD.value.setFeedbackLayout(changedContext, {"layout": "$A0"});
$SD.value.setFeedback(changedContext, {"full-canvas": image, "canvas": null, "title": ""})
} else {
$SD.value.setImage(changedContext, image)
Expand Down
9 changes: 9 additions & 0 deletions src/modules/common/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ export class Settings {
}

if (settings.version === 4) {
let settingsV5 = {...settings};
settingsV5.version = 5

settingsV5.display.useEncoderLayout = false;

return this.parse(settingsV5)
}

if (settings.version === 5) {
return settings;
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/modules/common/streamdeck.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ export class StreamDeck {
this.streamDeckWebsocket.send(JSON.stringify(message))
}

setFeedbackLayout(context, payload) {
let message = {
"event": "setFeedbackLayout",
"context": context,
"payload": payload
}

this.streamDeckWebsocket.send(JSON.stringify(message))
}

showAlert(context) {
let message = {
"event": "showAlert",
Expand Down
83 changes: 82 additions & 1 deletion src/modules/plugin/entityConfigFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,47 @@ export class EntityConfigFactory {
}
}

light = this.switch
light = {
"default": (state, attributes, templates) => {
let entityConfig = this.switch.default(state, attributes, templates);

if (attributes.supported_color_modes && attributes.supported_color_modes.includes("brightness")) {
entityConfig.rotationPercent = (attributes.brightness / 255.0) * 100;
entityConfig.icon = Mdi.mdiLightbulbOff;
if (state == 'on') {
if (entityConfig.rotationPercent > 90) {
entityConfig.icon = Mdi.mdiLightbulbOn;
} else if (entityConfig.rotationPercent > 80) {
entityConfig.icon = Mdi.mdiLightbulbOn90;
} else if (entityConfig.rotationPercent > 70) {
entityConfig.icon = Mdi.mdiLightbulbOn80;
} else if (entityConfig.rotationPercent > 60) {
entityConfig.icon = Mdi.mdiLightbulbOn70;
} else if (entityConfig.rotationPercent > 50) {
entityConfig.icon = Mdi.mdiLightbulbOn60;
} else if (entityConfig.rotationPercent > 40) {
entityConfig.icon = Mdi.mdiLightbulbOn50;
} else if (entityConfig.rotationPercent > 30) {
entityConfig.icon = Mdi.mdiLightbulbOn40;
} else if (entityConfig.rotationPercent > 20) {
entityConfig.icon = Mdi.mdiLightbulbOn30;
} else if (entityConfig.rotationPercent > 10) {
entityConfig.icon = Mdi.mdiLightbulbOn20;
} else {
entityConfig.icon = Mdi.mdiLightbulbOn10;
}
}

entityConfig.feedbackLayout = { layout: "$B1" };
entityConfig.feedback = {
value: Math.ceil(entityConfig.rotationPercent) + "%",
indicator: Math.ceil(entityConfig.rotationPercent)
};
}

return entityConfig;
}
}

input_boolean = this.switch

Expand Down Expand Up @@ -336,6 +376,47 @@ export class EntityConfigFactory {
}
}

"media_player" = {
"default": (state, attributes, templates) => {
let icon = Mdi.mdiVolumeOff;
let color = this.colors.passive;
let rotationPercent = 0;

if (state !== "off") {
if (attributes.is_volume_muted) {
icon = Mdi.mdiVolumeMute;
} else {
color = this.colors.active;
rotationPercent = attributes.volume_level * 100;
if (rotationPercent > 66) {
icon = Mdi.mdiVolumeHigh;
} else if (rotationPercent > 33) {
icon = Mdi.mdiVolumeMedium;
} else {
icon = Mdi.mdiVolumeLow;
}
}
}

let feedbackLayout = { layout: "$B1" };
let feedback = {
value: Math.ceil(rotationPercent) + "%",
indicator: Math.ceil(rotationPercent)
};

return {
state,
attributes,
templates,
icon,
color,
feedbackLayout,
feedback,
rotationPercent
}
}
}

vacuum = {
"default": (state, attributes, templates) => {
const icon = Mdi.mdiRobotVacuum;
Expand Down
16 changes: 16 additions & 0 deletions src/modules/plugin/svgUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ export class SvgUtils {
this.snap = Snap(this.buttonRes.width, this.buttonRes.height);
}

generateIconSVG(iconSVG, iconColor) {
const icon = this.snap.path(iconSVG)
icon.attr("fill", iconColor);
const iconBBox = icon.getBBox();
const iconHeight = iconBBox.height;
const iconWidth = iconBBox.width;
const targetHeight = this.buttonRes.height / 1.3;
const targetWidth = this.buttonRes.width / 1.3;
const scaleFactor = Math.min(targetHeight / iconHeight, targetWidth / iconWidth);
icon.transform(`scale(${scaleFactor})`)

let outerSVG = this.snap.outerSVG();
this.snap.clear();
return outerSVG
}

generateButtonSVG(labels, iconSVG, iconColor, isAction = false, isMultiAction = false) {

if (iconSVG) {
Expand Down

0 comments on commit aeb3ce2

Please sign in to comment.