Start adding extra views

This commit is contained in:
2026-06-19 00:06:31 +00:00
parent 0384879bea
commit 609938252b
10 changed files with 220 additions and 23 deletions

View File

@@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AlarmOverview: typeof import('./src/components/AlarmOverview.vue')['default']
Almanac: typeof import('./src/components/Almanac.vue')['default']
CalendarAgenda: typeof import('./src/components/CalendarAgenda.vue')['default']
CurrentLaundryStatus: typeof import('./src/components/CurrentLaundryStatus.vue')['default']

View File

@@ -51,7 +51,9 @@ spec:
valueFrom:
secretKeyRef:
name: display-internal-config
key: CALENDAR_EMBED_URL
key: CALENDAR_EMBED_URL
- name: ALARM_INTEGRATION
value: abode
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst

View File

@@ -4,4 +4,5 @@ sed -i -e "s~#API_PREFIX#~$API_PREFIX~g" /usr/share/nginx/html/assets/*
sed -i -e "s~#HOME_ASSISTANT_URL#~$HOME_ASSISTANT_URL~g" /usr/share/nginx/html/assets/*
sed -i -e "s~#HOME_ASSISTANT_TOKEN#~$HOME_ASSISTANT_TOKEN~g" /usr/share/nginx/html/assets/*
sed -i -e "s~#GARAGE_DEVICE#~$GARAGE_DEVICE~g" /usr/share/nginx/html/assets/*
sed -i -e "s~#ALARM_DEVICE#~$ALARM_DEVICE~g" /usr/share/nginx/html/assets/*
sed -i -e "s~#ALARM_DEVICE#~$ALARM_DEVICE~g" /usr/share/nginx/html/assets/*
sed -i -e "s~#ALARM_INTEGRATION#~$ALARM_INTEGRATION~g" /usr/share/nginx/html/assets/*

View File

@@ -1,7 +1,7 @@
{
"name": "web-display",
"version": "1.0.0",
"packageManager": "pnpm@10.29.3",
"packageManager": "pnpm@11.8.0",
"scripts": {
"dev": "cross-env NODE_OPTIONS='--no-warnings' vite",
"build": "vue-tsc --noEmit && vite build",

View File

@@ -0,0 +1,5 @@
allowBuilds:
'@parcel/watcher': true
core-js: true
esbuild: true
vue-demi: true

View File

@@ -0,0 +1,110 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useHomeAssistantStore } from '@/stores/homeAssistantStore';
const ready = ref(false);
const homeAssistantStore = useHomeAssistantStore();
homeAssistantStore.start();
const sortedAlarmEntities = computed(() => {
return Object.entries(homeAssistantStore.alarmEntities)
.map(([key, value]) => ({
key,
...value
}))
.sort((a, b) => {
const deviceClassA = a.attributes.device_class || '';
const deviceClassB = b.attributes.device_class || '';
if (deviceClassA === deviceClassB) {
const nameA = a.attributes.friendly_name || '';
const nameB = b.attributes.friendly_name || '';
return nameA.localeCompare(nameB);
}
return deviceClassA.localeCompare(deviceClassB);
});
});
function translateAlarmState(state: string): string {
switch (state) {
case 'on':
return 'Open';
case 'off':
return 'Closed';
default:
return 'Unknown';
}
}
ready.value = true;
</script>
<template>
<div
class="alarm-overview"
v-if="ready">
<div class="alarm-header">Doors and Windows</div>
<div v-for="alarmEntity in sortedAlarmEntities">
<div
class="alarm-device"
v-if="alarmEntity.attributes.device_class === 'door'">
<v-icon
class="kiosk-alarm-device-icon"
:icon="alarmEntity.state === 'on' ? 'mdi-door-open' : 'mdi-door-closed'"
:class="{ 'kiosk-alarm-device-open': alarmEntity.state === 'on' }" />
{{ alarmEntity.attributes.friendly_name }}
<div class="alarm-device-state">
{{ translateAlarmState(alarmEntity.state) }}
</div>
</div>
<div
class="alarm-device"
v-if="alarmEntity.attributes.device_class === 'window'">
<v-icon
class="kiosk-device-icon"
:icon="alarmEntity.state === 'on' ? 'mdi-window-open' : 'mdi-window-closed'"
:class="{ 'kiosk-alarm-device-open': alarmEntity.state === 'on' }" />
{{ alarmEntity.attributes.friendly_name }}
<div class="alarm-device-state">
{{ translateAlarmState(alarmEntity.state) }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.alarm-overview {
background-color: #121212;
border-radius: 10px;
display: flex;
flex: 1;
flex-direction: column;
padding: 10px;
}
.alarm-header {
font-size: 1.15em;
padding-bottom: 2px;
text-align: center;
}
.kiosk-alarm-device-open {
color: #d09f27;
}
.alarm-device {
padding: 4px 0;
}
.alarm-device-state {
float: right;
}
</style>

View File

@@ -18,4 +18,8 @@ export default class Environment {
public static getAlarmDevice(): string {
return '#ALARM_DEVICE#';
}
public static getAlarmIntegration(): string {
return '#ALARM_INTEGRATION#';
}
}

View File

@@ -0,0 +1,4 @@
export interface HomeAssistantRegistryEntity {
entity_id: string;
platform: string;
}

View File

@@ -7,6 +7,14 @@
import CalendarAgenda from '@/components/CalendarAgenda.vue';
import LongPressButton from '@/components/LongPressButton.vue';
import PressureTrendArrow from '@/components/PressureTrendArrow.vue';
import AlarmOverview from '@/components/AlarmOverview.vue';
enum KioskPage {
Calendar,
AlarmOverview
}
const kioskPage = ref(KioskPage.Calendar);
const outOfDateDuration = 5000;
@@ -233,11 +241,34 @@
</div>
</div>
<div class="kiosk-content">
<CalendarAgenda
class="kiosk-calendar"
days="10"
:refresh-interval="5 * 60 * 1000" />
<NationalDays class="kiosk-national-days" />
<div class="kiosk-navigation">
<v-icon
class="kiosk-navigation-icon"
icon="mdi-calendar"
:class="{ 'kiosk-navigation-selected': kioskPage === KioskPage.Calendar }"
@click="kioskPage = KioskPage.Calendar" />
<v-icon
class="kiosk-navigation-icon"
icon="mdi-shield-home-outline"
:class="{ 'kiosk-navigation-selected': kioskPage === KioskPage.AlarmOverview }"
@click="kioskPage = KioskPage.AlarmOverview" />
</div>
<div
class="kiosk-calendar-page"
v-show="kioskPage === KioskPage.Calendar">
<CalendarAgenda
class="kiosk-calendar"
days="10"
:refresh-interval="5 * 60 * 1000" />
<NationalDays
class="kiosk-national-days"
v-show="kioskPage === KioskPage.Calendar" />
</div>
<div
class="kiosk-alarm-overview-page"
v-show="kioskPage === KioskPage.AlarmOverview">
<AlarmOverview class="kiosk-alarm-overview" />
</div>
</div>
</v-container>
</template>
@@ -286,18 +317,25 @@
.kiosk-content {
height: 100%;
max-height: calc(100vh - 30px);
max-height: calc(100vh);
padding: 10px;
gap: 10px;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(4, 25%);
grid-auto-flow: row;
grid-template-areas:
'kiosk-calendar kiosk-national-days kiosk-national-days'
'kiosk-calendar kiosk-national-days kiosk-national-days'
'kiosk-calendar kiosk-national-days kiosk-national-days'
'kiosk-calendar kiosk-national-days kiosk-national-days';
}
.kiosk-navigation {
background-color: #121212;
border-radius: 10px;
align-content: center;
padding: 0 10px;
height: 50px;
}
.kiosk-navigation-icon {
font-size: 2rem;
margin-right: 10px;
}
.kiosk-navigation-selected {
color: #5e83c7;
}
.kiosk-time {
@@ -362,12 +400,27 @@
font-size: 1.3rem;
}
.kiosk-calendar-page {
display: flex;
padding-top: 10px;
gap: 10px;
height: calc(100vh - 70px);
}
.kiosk-alarm-overview-page {
display: flex;
padding-top: 10px;
gap: 10px;
height: calc(100vh - 70px);
}
.kiosk-calendar {
grid-area: kiosk-calendar;
flex-basis: 20%;
}
.kiosk-national-days {
grid-area: kiosk-national-days;
flex-grow: 1;
flex-basis: auto;
}
.warning {

View File

@@ -1,12 +1,14 @@
import { defineStore } from 'pinia';
import { createConnection, subscribeEntities, createLongLivedTokenAuth, Connection, callService } from 'home-assistant-js-websocket';
import { createConnection, subscribeEntities, createLongLivedTokenAuth, Connection, callService, HassEntities, HassEntity } from 'home-assistant-js-websocket';
import Environment from '@/environment';
import { HomeAssistantRegistryEntity } from '@/models/homeAssistant/registryEntity';
export const useHomeAssistantStore = defineStore('home-assistant', {
state: () => {
return {
garageState: null as string | null,
houseAlarmState: null as string | null,
alarmEntities: {} as Record<string, HassEntity>,
_connection: null as Connection | null
};
},
@@ -18,12 +20,21 @@ export const useHomeAssistantStore = defineStore('home-assistant', {
const garageDevice = Environment.getGarageDevice();
const alarmDevice = Environment.getAlarmDevice();
const alarmIntegration = Environment.getAlarmIntegration();
const auth = createLongLivedTokenAuth(Environment.getHomeAssistantUrl(), Environment.getHomeAssistantToken());
this._connection = await createConnection({ auth });
subscribeEntities(this._connection as Connection, (entities) => {
const homeAssistantRegistryEntries: HomeAssistantRegistryEntity[] = await this._connection.sendMessagePromise({
type: 'config/entity_registry/list'
});
const alarmEntityList = homeAssistantRegistryEntries.filter((entity) => entity.platform === alarmIntegration);
const alarmEntities: Record<string, HomeAssistantRegistryEntity> = Object.fromEntries(alarmEntityList.map((entity) => [entity.entity_id, entity]));
subscribeEntities(this._connection as Connection, (entities: HassEntities) => {
const garageEntity = entities[garageDevice];
if (garageEntity) {
@@ -35,6 +46,12 @@ export const useHomeAssistantStore = defineStore('home-assistant', {
if (houseAlarmEntity) {
this.$patch({ houseAlarmState: houseAlarmEntity.state });
}
Object.entries(entities).forEach(([entityId, entity]) => {
if (alarmEntities[entityId]) {
this.$patch((state) => (state.alarmEntities[entityId] = entity));
}
});
});
setInterval(async () => await this._connection?.ping(), 5000);