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

Initial encoder layout support #278

Merged
merged 1 commit into from
Jun 26, 2024
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
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
Loading