mirror of
https://github.com/ckaczor/HomeMonitor.git
synced 2026-01-13 17:22:54 -05:00
Kiosk improvements
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"axios": "^1.7.9",
|
||||
"core-js": "^3.39.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"home-assistant-js-websocket": "^9.4.0",
|
||||
"pinia": "^2.3.0",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"suncalc": "^1.9.0",
|
||||
@@ -45,7 +46,6 @@
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-tsc": "^2.1.10",
|
||||
"websocketstream-polyfill": "^1.0.1"
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
16
WebDisplay/pnpm-lock.yaml
generated
16
WebDisplay/pnpm-lock.yaml
generated
@@ -32,6 +32,9 @@ importers:
|
||||
date-fns:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
home-assistant-js-websocket:
|
||||
specifier: ^9.4.0
|
||||
version: 9.4.0
|
||||
pinia:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(typescript@5.6.2)(vue@3.5.13(typescript@5.6.2))
|
||||
@@ -114,9 +117,6 @@ importers:
|
||||
vue-tsc:
|
||||
specifier: ^2.1.10
|
||||
version: 2.1.10(typescript@5.6.2)
|
||||
websocketstream-polyfill:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1310,6 +1310,9 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
home-assistant-js-websocket@9.4.0:
|
||||
resolution: {integrity: sha512-312TuI63IfKf8G+iWvKmPYIdxWMNojwVk03o9OSpQFFDjSCNAYdCUfuPCFs73SuJ1Xpd4D1Eo11CB33MGMqZ+Q==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -2044,9 +2047,6 @@ packages:
|
||||
webpack-virtual-modules@0.6.2:
|
||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||
|
||||
websocketstream-polyfill@1.0.1:
|
||||
resolution: {integrity: sha512-SV0AG8yN3YDwCVCex/oBQli7vrezGQ0stgo5suKMTthjPQv2wuMsNvwUWDAvNmDGGb9VIm5WsW99x57GW06gLQ==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
@@ -3406,6 +3406,8 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
home-assistant-js-websocket@9.4.0: {}
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
immutable@5.0.3: {}
|
||||
@@ -4167,8 +4169,6 @@ snapshots:
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
websocketstream-polyfill@1.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
|
||||
11
WebDisplay/src/models/calendar/calendar-day.ts
Normal file
11
WebDisplay/src/models/calendar/calendar-day.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import CalendarEntry from './calendar-entry';
|
||||
|
||||
export default class CalendarDay {
|
||||
date: Date;
|
||||
entries: CalendarEntry[];
|
||||
|
||||
constructor(date: Date, entries: CalendarEntry[]) {
|
||||
this.date = date;
|
||||
this.entries = entries;
|
||||
}
|
||||
}
|
||||
6
WebDisplay/src/models/calendar/calendar-entry.ts
Normal file
6
WebDisplay/src/models/calendar/calendar-entry.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface CalendarEntry {
|
||||
summary: string;
|
||||
isAllDay: boolean;
|
||||
start: Date;
|
||||
end: Date;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
let messageId: number = 1;
|
||||
|
||||
export enum MessageType {
|
||||
auth_invalid = 'auth_invalid',
|
||||
auth_ok = 'auth_ok',
|
||||
auth_required = 'auth_required',
|
||||
event = 'event',
|
||||
pong = 'pong',
|
||||
result = 'result'
|
||||
}
|
||||
|
||||
export interface OutgoingMessage {
|
||||
id: number | undefined;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IncomingMessage {
|
||||
id: number | undefined;
|
||||
type: string;
|
||||
success: boolean | undefined;
|
||||
result: any;
|
||||
}
|
||||
|
||||
export class AuthMessage implements OutgoingMessage {
|
||||
id = undefined;
|
||||
type: string = 'auth';
|
||||
access_token: string | null = null;
|
||||
|
||||
constructor(access_token: string) {
|
||||
this.access_token = access_token;
|
||||
}
|
||||
}
|
||||
|
||||
export class SubscribeEntitiesMessage implements OutgoingMessage {
|
||||
id = messageId++;
|
||||
type = 'subscribe_entities';
|
||||
entity_ids: string[] = [];
|
||||
|
||||
constructor(entity_ids: string[]) {
|
||||
this.entity_ids = entity_ids;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EventMessage extends IncomingMessage {
|
||||
event: StatesUpdates;
|
||||
}
|
||||
|
||||
export interface EntityState {
|
||||
/** state */
|
||||
s: string;
|
||||
/** attributes */
|
||||
a: { [key: string]: any };
|
||||
/** last_changed; if set, also applies to lu */
|
||||
lc: number;
|
||||
/** last_updated */
|
||||
lu: number;
|
||||
}
|
||||
|
||||
export interface StatesUpdates {
|
||||
/** add */
|
||||
a?: Record<string, EntityState>;
|
||||
/** remove */
|
||||
r?: string[]; // remove
|
||||
}
|
||||
@@ -4,7 +4,13 @@
|
||||
import { useLaundryStore } from '@/stores/laundryStore';
|
||||
import { usePowerStore } from '@/stores/powerStore';
|
||||
import { useHomeAssistantStore } from '@/stores/homeAssistantStore';
|
||||
import Environment from '@/environment';
|
||||
import { useCalendarStore } from '@/stores/calendarStore';
|
||||
import { format, startOfDay, endOfDay } from 'date-fns';
|
||||
import CalendarDay from '@/models/calendar/calendar-day';
|
||||
|
||||
const calendarDayCount = 7;
|
||||
|
||||
const calendarReady = ref(false);
|
||||
|
||||
const weatherStore = useWeatherStore();
|
||||
weatherStore.start();
|
||||
@@ -18,12 +24,61 @@
|
||||
const homeAssistantStore = useHomeAssistantStore();
|
||||
homeAssistantStore.start();
|
||||
|
||||
const calendarStore = useCalendarStore();
|
||||
|
||||
const currentTime = ref(new Date());
|
||||
const calendarDays = ref([] as CalendarDay[]);
|
||||
|
||||
const timeFormatter = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', { weekday: 'long', month: 'long', day: 'numeric' });
|
||||
|
||||
setInterval(() => (currentTime.value = new Date()), 1000);
|
||||
function alarmState(state: string): string {
|
||||
switch (state) {
|
||||
case 'armed_home':
|
||||
return 'Armed';
|
||||
case 'armed_away':
|
||||
return 'Armed';
|
||||
case 'disarmed':
|
||||
return 'Disarmed';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function loadCalendar() {
|
||||
const newCalendarDays = [] as CalendarDay[];
|
||||
|
||||
calendarStore.getUpcoming(calendarDayCount).then((upcoming) => {
|
||||
const currentDay = startOfDay(currentTime.value);
|
||||
|
||||
for (let i = 0; i < calendarDayCount; i++) {
|
||||
const day = new Date(currentDay);
|
||||
day.setDate(day.getDate() + i);
|
||||
|
||||
const entries = upcoming.filter((entry) => {
|
||||
const entryStart = startOfDay(entry.start);
|
||||
const entryEnd = endOfDay(entry.end);
|
||||
|
||||
if (entry.isAllDay) {
|
||||
return day > entryStart && day < entryEnd;
|
||||
}
|
||||
|
||||
return day >= entryStart && day <= entryEnd;
|
||||
});
|
||||
|
||||
newCalendarDays.push(new CalendarDay(day, entries));
|
||||
}
|
||||
|
||||
calendarDays.value = newCalendarDays;
|
||||
|
||||
calendarReady.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
loadCalendar();
|
||||
|
||||
setInterval(() => currentTime.value = new Date(), 1000);
|
||||
setInterval(() => loadCalendar(), 60000);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -108,19 +163,38 @@
|
||||
class="kiosk-device-icon"
|
||||
icon="mdi-shield-home" />
|
||||
<div class="kiosk-device-text">
|
||||
{{ capitalize(homeAssistantStore.houseAlarmState) }}
|
||||
{{ alarmState(homeAssistantStore.houseAlarmState) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kiosk-content" v-if="Environment.getCalendarEmbedUrl()">
|
||||
<div class="kiosk-calendar">
|
||||
<iframe
|
||||
:src="Environment.getCalendarEmbedUrl()"
|
||||
style="border-width: 0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
scrolling="no"></iframe>
|
||||
<div class="kiosk-content">
|
||||
<div
|
||||
class="kiosk-calendar"
|
||||
v-if="calendarReady">
|
||||
<div class="kiosk-calendar-header">
|
||||
{{ 'Next ' + calendarDayCount + ' Days' }}
|
||||
</div>
|
||||
<ul class="kiosk-calendar-day-list">
|
||||
<li
|
||||
class="kiosk-calendar-day-item"
|
||||
v-for="calendarDay in calendarDays">
|
||||
<div>
|
||||
<span class="kiosk-calendar-day-item-number">
|
||||
{{ format(calendarDay.date, 'dd') }}
|
||||
</span>
|
||||
<span class="kiosk-calendar-day-item-name">
|
||||
{{ format(calendarDay.date, 'MMMM, EEEE') }}
|
||||
</span>
|
||||
<ul
|
||||
class="kiosk-calendar-entry"
|
||||
v-for="calendarEntry in calendarDay.entries">
|
||||
<span>
|
||||
{{ calendarEntry.summary }}
|
||||
</span>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
@@ -131,7 +205,7 @@
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
background-color: #212428;
|
||||
color: #1f1f1f;
|
||||
color: #ebebeb;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
@@ -169,12 +243,15 @@
|
||||
|
||||
.kiosk-content {
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: 'kiosk-calendar .';
|
||||
grid-template-areas:
|
||||
'kiosk-calendar . .'
|
||||
'kiosk-calendar . .';
|
||||
}
|
||||
|
||||
.kiosk-time {
|
||||
@@ -231,6 +308,48 @@
|
||||
|
||||
.kiosk-calendar {
|
||||
grid-area: kiosk-calendar;
|
||||
background-color: #121212;
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.kiosk-calendar-header {
|
||||
font-size: 1.15em;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kiosk-calendar-day-item-number {
|
||||
font-size: 1.25em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.kiosk-calendar-day-list {
|
||||
margin-left: 10px;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kiosk-calendar-day-item:not(:last-child) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.kiosk-calendar-day-item:first-of-type {
|
||||
color: #c75ec7;
|
||||
}
|
||||
|
||||
.kiosk-calendar-day-item:not(:first-child) {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.kiosk-calendar-entry {
|
||||
color: #ebebeb;
|
||||
padding-left: 2em;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.true {
|
||||
|
||||
17
WebDisplay/src/stores/calendarStore.ts
Normal file
17
WebDisplay/src/stores/calendarStore.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import axios from 'axios';
|
||||
import Environment from '@/environment';
|
||||
import CalendarEntry from '@/models/calendar/calendar-entry';
|
||||
|
||||
export const useCalendarStore = defineStore('calendar', {
|
||||
state: () => {
|
||||
return {};
|
||||
},
|
||||
actions: {
|
||||
async getUpcoming(days: number): Promise<CalendarEntry[]> {
|
||||
const response = await axios.get<CalendarEntry[]>(Environment.getUrlPrefix() + `:8081/api/calendar/calendar/upcoming?days=${days}`);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,5 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { WebSocketStream } from 'websocketstream-polyfill';
|
||||
import { MessageType, AuthMessage, IncomingMessage, SubscribeEntitiesMessage, EventMessage } from '@/models/home-assistant/home-assistant';
|
||||
|
||||
import { createConnection, subscribeEntities, createLongLivedTokenAuth, Connection } from 'home-assistant-js-websocket';
|
||||
import Environment from '@/environment';
|
||||
|
||||
export const useHomeAssistantStore = defineStore('home-assistant', {
|
||||
@@ -9,7 +7,7 @@ export const useHomeAssistantStore = defineStore('home-assistant', {
|
||||
return {
|
||||
garageState: null as string | null,
|
||||
houseAlarmState: null as string | null,
|
||||
_wss: null as WebSocketStream | null
|
||||
_connection: null as Connection | null
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
@@ -21,72 +19,27 @@ export const useHomeAssistantStore = defineStore('home-assistant', {
|
||||
const garageDevice = Environment.getGarageDevice();
|
||||
const alarmDevice = Environment.getAlarmDevice();
|
||||
|
||||
this._wss = new WebSocketStream(Environment.getHomeAssistantUrl());
|
||||
const auth = createLongLivedTokenAuth(Environment.getHomeAssistantUrl(), Environment.getHomeAssistantToken());
|
||||
|
||||
const { readable, writable } = await this._wss.opened;
|
||||
this._connection = await createConnection({ auth });
|
||||
|
||||
const reader = readable.getReader();
|
||||
const writer = writable.getWriter();
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
const message = JSON.parse(value as string) as IncomingMessage;
|
||||
|
||||
console.info(message);
|
||||
|
||||
switch (message.type) {
|
||||
case MessageType.auth_required:
|
||||
const authMessage = new AuthMessage(Environment.getHomeAssistantToken());
|
||||
writer.write(JSON.stringify(authMessage));
|
||||
break;
|
||||
case MessageType.auth_ok:
|
||||
const subscribeEntitiesMessage = new SubscribeEntitiesMessage([
|
||||
garageDevice,
|
||||
alarmDevice
|
||||
]);
|
||||
writer.write(JSON.stringify(subscribeEntitiesMessage));
|
||||
break;
|
||||
case MessageType.event:
|
||||
const eventMessage = message as EventMessage;
|
||||
|
||||
if (!eventMessage?.event?.a) {
|
||||
break;
|
||||
}
|
||||
|
||||
const garageEntity = eventMessage.event.a[garageDevice];
|
||||
subscribeEntities(this._connection as Connection, entities => {
|
||||
const garageEntity = entities[garageDevice];
|
||||
|
||||
if (garageEntity) {
|
||||
this.$patch({ garageState: garageEntity.s });
|
||||
this.$patch({ garageState: garageEntity.state });
|
||||
}
|
||||
|
||||
const houseAlarmEntity = eventMessage.event.a[alarmDevice];
|
||||
const houseAlarmEntity = entities[alarmDevice];
|
||||
|
||||
if (houseAlarmEntity) {
|
||||
this.$patch({ houseAlarmState: houseAlarmEntity.s });
|
||||
}
|
||||
|
||||
break;
|
||||
case MessageType.result:
|
||||
// Handle result type
|
||||
break;
|
||||
default:
|
||||
// Handle unknown message type
|
||||
break;
|
||||
}
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
this.$patch({ houseAlarmState: houseAlarmEntity.state });
|
||||
}
|
||||
});
|
||||
},
|
||||
async stop() {
|
||||
if (!this._wss) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._wss.close();
|
||||
this._wss = null;
|
||||
this._connection?.close();
|
||||
this._connection = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user