284 lines
7.8 KiB
TypeScript
284 lines
7.8 KiB
TypeScript
import {
|
|
type API_HA_DeskPosition,
|
|
BaseService,
|
|
type ComponentUpdate,
|
|
createComponentUpdate,
|
|
type FullInformation,
|
|
type GristRecord_PersonalGoals,
|
|
type HomeAssistantDeskPositionResult,
|
|
type ServiceResult,
|
|
StatsComponent,
|
|
type TidalGetCurrent,
|
|
type WsService,
|
|
} from "@dpu/shared";
|
|
import { logInfo, logWarning } from "@dpu/shared/dist/logger.js";
|
|
import { DateTime } from "luxon";
|
|
import type { WebSocket } from "ws";
|
|
import type { GristService } from "../grist/service";
|
|
import type { HomeAssistantService } from "../homeassistant/service";
|
|
import type { TidalService } from "../tidal/service";
|
|
|
|
export class HomepageService extends BaseService<null> {
|
|
private gristService: GristService;
|
|
private haService: HomeAssistantService;
|
|
private tidalService: TidalService;
|
|
private wsService: WsService;
|
|
private lastPoll: FullInformation = {
|
|
ha_desk_position: null,
|
|
ha_temp: null,
|
|
tidal_current: null,
|
|
grist_personal_goals: null,
|
|
};
|
|
|
|
constructor(
|
|
gristService: GristService,
|
|
haService: HomeAssistantService,
|
|
tidalService: TidalService,
|
|
wsService: WsService,
|
|
) {
|
|
super(null);
|
|
this.gristService = gristService;
|
|
this.haService = haService;
|
|
this.tidalService = tidalService;
|
|
this.wsService = wsService;
|
|
}
|
|
|
|
scheduleMidnightGristUpdate(): void {
|
|
const now = DateTime.now().setZone("Europe/Berlin");
|
|
const nextMidnight = now.plus({ days: 1 }).startOf("day");
|
|
|
|
setTimeout(async () => {
|
|
await this.fetchAndBroadcastGrist();
|
|
this.scheduleMidnightGristUpdate();
|
|
}, nextMidnight.diff(now).toMillis());
|
|
}
|
|
|
|
private async fetchAndBroadcastGrist(): Promise<void> {
|
|
try {
|
|
if (this.wsService.getClientCount() > 0) {
|
|
const updates: ComponentUpdate[] = [];
|
|
this.updateGristPG(await this._getGristPG(), updates);
|
|
|
|
if (updates.length > 0) {
|
|
this.wsService.broadcast({
|
|
type: "update",
|
|
data: updates,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logWarning("Error fetching midnight Grist update", error);
|
|
}
|
|
}
|
|
|
|
async sendFullInformationToSocket(socket: WebSocket): Promise<void> {
|
|
try {
|
|
const [desk, temp, tidal, personal_goals] = await this._getAll();
|
|
const updates: ComponentUpdate[] = [
|
|
createComponentUpdate(StatsComponent.HA_DESK_POSITION, desk),
|
|
createComponentUpdate(StatsComponent.HA_TEMP, temp),
|
|
createComponentUpdate(StatsComponent.TIDAL_CURRENT, tidal),
|
|
createComponentUpdate(
|
|
StatsComponent.GRIST_PERSONAL_GOALS,
|
|
personal_goals,
|
|
),
|
|
];
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "update",
|
|
data: updates,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
logWarning("error getting all information for socket update.", error);
|
|
}
|
|
}
|
|
|
|
async getFullInformation(): Promise<ServiceResult<FullInformation | string>> {
|
|
try {
|
|
const [desk, temp, tidal, personal_goals] = await this._getAll();
|
|
|
|
return this.getSuccessfulResult(
|
|
this.updateFull({
|
|
ha_desk_position: desk as API_HA_DeskPosition,
|
|
ha_temp: temp as string,
|
|
tidal_current: tidal as TidalGetCurrent,
|
|
grist_personal_goals: personal_goals as GristRecord_PersonalGoals,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
return this.getErrorResult("error getting all information.", error);
|
|
}
|
|
}
|
|
|
|
private async _getAll(): Promise<Iterable<unknown>> {
|
|
return await Promise.all([
|
|
this._getDesk(),
|
|
this._getTemp(),
|
|
this._getTidal(),
|
|
this._getGristPG(),
|
|
]);
|
|
}
|
|
|
|
private async _getDesk(): Promise<API_HA_DeskPosition | null> {
|
|
const desk = await this.haService.getDeskPosition();
|
|
return desk.successful
|
|
? this.haService.convertPosResultToApiAnswer(
|
|
desk.result as HomeAssistantDeskPositionResult,
|
|
)
|
|
: null;
|
|
}
|
|
|
|
private async _getTemp(): Promise<string | null> {
|
|
const temp = await this.haService.getTemperature();
|
|
return temp.successful ? temp.result : null;
|
|
}
|
|
|
|
private async _getTidal(): Promise<TidalGetCurrent | null> {
|
|
const tidal = await this.tidalService.getSong();
|
|
return tidal ? (tidal.result as TidalGetCurrent) : null;
|
|
}
|
|
|
|
private async _getGristPG(): Promise<GristRecord_PersonalGoals | null> {
|
|
const personal_goals = await this.gristService.getToday();
|
|
return personal_goals.successful
|
|
? (personal_goals.result as GristRecord_PersonalGoals)
|
|
: null;
|
|
}
|
|
|
|
updateFull(newPoll: FullInformation): FullInformation {
|
|
const updates: ComponentUpdate[] = [];
|
|
|
|
this.updateHaDesk(newPoll.ha_desk_position, updates);
|
|
this.updateHaTemp(newPoll.ha_temp, updates);
|
|
this.updateTidal(newPoll.tidal_current, updates);
|
|
this.updateGristPG(newPoll.grist_personal_goals, updates);
|
|
|
|
if (updates.length > 0) {
|
|
logInfo("Updating clients");
|
|
this.wsService.broadcast({
|
|
type: "update",
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
return newPoll;
|
|
}
|
|
|
|
async updatePartial(
|
|
components: ComponentUpdate[],
|
|
): Promise<ServiceResult<string>> {
|
|
const updates: ComponentUpdate[] = [];
|
|
for (const component of components) {
|
|
switch (component.component) {
|
|
case StatsComponent.HA_DESK_POSITION: {
|
|
this.updateHaDesk(
|
|
(component.data as API_HA_DeskPosition) ?? (await this._getDesk()),
|
|
updates,
|
|
);
|
|
break;
|
|
}
|
|
case StatsComponent.HA_TEMP:
|
|
this.updateHaTemp(
|
|
(component.data as string) ?? (await this._getTemp()),
|
|
updates,
|
|
);
|
|
break;
|
|
case StatsComponent.TIDAL_CURRENT:
|
|
this.updateTidal(
|
|
(component.data as TidalGetCurrent) ?? (await this._getTidal()),
|
|
updates,
|
|
);
|
|
break;
|
|
case StatsComponent.GRIST_PERSONAL_GOALS:
|
|
this.updateGristPG(
|
|
(component.data as GristRecord_PersonalGoals) ??
|
|
(await this._getGristPG()),
|
|
updates,
|
|
);
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
|
|
this.wsService.broadcast({
|
|
type: "update",
|
|
data: updates,
|
|
});
|
|
|
|
return this.getSuccessfulResult("update broadcasted");
|
|
}
|
|
|
|
updateHaDesk(
|
|
new_ha_desk_position: API_HA_DeskPosition | null,
|
|
updates: ComponentUpdate[],
|
|
): void {
|
|
if (
|
|
this.lastPoll.ha_desk_position?.is_standing !==
|
|
new_ha_desk_position?.is_standing
|
|
) {
|
|
this.lastPoll.ha_desk_position = new_ha_desk_position;
|
|
updates.push(
|
|
createComponentUpdate(
|
|
StatsComponent.HA_DESK_POSITION,
|
|
new_ha_desk_position,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
updateHaTemp(new_ha_temp: string | null, updates: ComponentUpdate[]): void {
|
|
if (this.lastPoll.ha_temp !== new_ha_temp) {
|
|
this.lastPoll.ha_temp = new_ha_temp;
|
|
updates.push(createComponentUpdate(StatsComponent.HA_TEMP, new_ha_temp));
|
|
}
|
|
}
|
|
|
|
updateTidal(
|
|
new_tidal_current: TidalGetCurrent | null,
|
|
updates: ComponentUpdate[],
|
|
): void {
|
|
if (
|
|
this.lastPoll.tidal_current?.title !== new_tidal_current?.title ||
|
|
this.lastPoll.tidal_current?.artists !== new_tidal_current?.artists ||
|
|
this.lastPoll.tidal_current?.status !== new_tidal_current?.status ||
|
|
this.lastPoll.tidal_current?.volume !== new_tidal_current?.volume
|
|
) {
|
|
this.lastPoll.tidal_current = new_tidal_current;
|
|
updates.push(
|
|
createComponentUpdate(StatsComponent.TIDAL_CURRENT, new_tidal_current),
|
|
);
|
|
}
|
|
}
|
|
|
|
updateGristPG(
|
|
new_grist_personal_goals: GristRecord_PersonalGoals | null,
|
|
updates: ComponentUpdate[],
|
|
): void {
|
|
if (
|
|
this.lastPoll.grist_personal_goals?.went_outside !==
|
|
new_grist_personal_goals?.went_outside ||
|
|
this.lastPoll.grist_personal_goals?.standing !==
|
|
new_grist_personal_goals?.standing ||
|
|
this.lastPoll.grist_personal_goals?.steps !==
|
|
new_grist_personal_goals?.steps ||
|
|
this.lastPoll.grist_personal_goals?.pushups !==
|
|
new_grist_personal_goals?.pushups ||
|
|
this.lastPoll.grist_personal_goals?.squats !==
|
|
new_grist_personal_goals?.squats ||
|
|
this.lastPoll.grist_personal_goals?.leg_raises !==
|
|
new_grist_personal_goals?.leg_raises ||
|
|
this.lastPoll.grist_personal_goals?.stairs !==
|
|
new_grist_personal_goals?.stairs
|
|
) {
|
|
this.lastPoll.grist_personal_goals = new_grist_personal_goals;
|
|
updates.push(
|
|
createComponentUpdate(
|
|
StatsComponent.GRIST_PERSONAL_GOALS,
|
|
new_grist_personal_goals,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|