mirror of
https://github.com/ckaczor/HomeMonitor.git
synced 2026-06-19 19:55:08 -04:00
Start adding extra views
This commit is contained in:
1
WebDisplay/components.d.ts
vendored
1
WebDisplay/components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/*
|
||||
@@ -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",
|
||||
|
||||
5
WebDisplay/pnpm-workspace.yaml
Normal file
5
WebDisplay/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': true
|
||||
core-js: true
|
||||
esbuild: true
|
||||
vue-demi: true
|
||||
110
WebDisplay/src/components/AlarmOverview.vue
Normal file
110
WebDisplay/src/components/AlarmOverview.vue
Normal 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>
|
||||
@@ -18,4 +18,8 @@ export default class Environment {
|
||||
public static getAlarmDevice(): string {
|
||||
return '#ALARM_DEVICE#';
|
||||
}
|
||||
|
||||
public static getAlarmIntegration(): string {
|
||||
return '#ALARM_INTEGRATION#';
|
||||
}
|
||||
}
|
||||
|
||||
4
WebDisplay/src/models/homeAssistant/registryEntity.ts
Normal file
4
WebDisplay/src/models/homeAssistant/registryEntity.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface HomeAssistantRegistryEntity {
|
||||
entity_id: string;
|
||||
platform: string;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user