Skip to content

Commit

Permalink
Add a widget that indicates activity state for a given zone
Browse files Browse the repository at this point in the history
  • Loading branch information
Erikvl87 committed Nov 4, 2024
1 parent 048a4ea commit 30e5758
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 9 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
}
],
"@typescript-eslint/explicit-function-return-type": "error", // Enforce explicit return type
"@typescript-eslint/no-floating-promises": "error", // Enforce awaiting async functions
"@typescript-eslint/no-floating-promises": "error", // Enforce awaiting async functions,
"@typescript-eslint/no-explicit-any": "warn", // Warn on explicit any
"indent": [
"error",
"tab"
Expand Down
2 changes: 1 addition & 1 deletion .homeycompose/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/Erikvl87/nl.erikvl87.zoneactivity/issues"
},
"source": "https://github.com/Erikvl87/nl.erikvl87.zoneactivity",
"compatibility": ">=5.0.0",
"compatibility": ">=12.1.2",
"sdk": 3,
"platforms": [
"local"
Expand Down
33 changes: 31 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"url": "https://github.com/Erikvl87/nl.erikvl87.zoneactivity/issues"
},
"source": "https://github.com/Erikvl87/nl.erikvl87.zoneactivity",
"compatibility": ">=5.0.0",
"compatibility": ">=12.1.2",
"sdk": 3,
"platforms": [
"local"
Expand Down Expand Up @@ -628,5 +628,34 @@
}
]
}
]
],
"widgets": {
"zone-activity-state": {
"name": {
"en": "Zone activity state",
"nl": "Zone activiteit status"
},
"height": 75,
"settings": [
{
"id": "zone",
"type": "autocomplete",
"title": {
"en": "Zone"
}
}
],
"api": {
"getZone": {
"method": "GET",
"path": "/getZone"
},
"log": {
"method": "POST",
"path": "/log"
}
},
"id": "zone-activity-state"
}
}
}
4 changes: 4 additions & 0 deletions app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ConditionCardEvaluateSensorCapabilities from './lib/ConditionCardEvaluate
import ConditionCardZoneActiveForMinutes from './lib/ConditionCardZoneActiveForMinutes';
import TriggerCardAnyDeviceTurnedOn from './lib/TriggerCardAnyDeviceOnOff';
import ZonesDb from './lib/ZonesDb';
import WidgetZoneActivityState from './lib/WidgetZoneActivityState';

class ZoneActivity extends Homey.App {
/**
Expand All @@ -16,6 +17,8 @@ class ZoneActivity extends Homey.App {
homeyApi!: ExtendedHomeyAPIV3Local;
homeyLog?: Log;

private widget!: WidgetZoneActivityState;

/**
* Initialize the Zone Activity app.
* @returns {Promise<void>} A promise that resolves when the app has been initialized.
Expand All @@ -34,6 +37,7 @@ class ZoneActivity extends Homey.App {
await ConditionCardZoneActiveForMinutes.initialize(this.homey, this.homeyApi, zonesDb, this.log);
await ConditionCardEvaluateSensorCapabilities.initialize(this.homey, this.homeyApi, zonesDb, this.log);
await TriggerCardAnyDeviceTurnedOn.initialize(this.homey, this.homeyApi, zonesDb, this.log, this.error);
this.widget = await WidgetZoneActivityState.initialize(this.homey, this.homeyApi, zonesDb, this.log, this.error);
}
}

Expand Down
65 changes: 65 additions & 0 deletions lib/WidgetZoneActivityState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ExtendedHomeyAPIV3Local, ExtendedZone } from "homey-api";
import Homey from "homey/lib/Homey";
import handleZoneAutocomplete from "../utils/handleZoneAutocomplete";
import ZonesDb from "./ZonesDb";
import getIconForZone, { getZoneImageSource } from "./../utils/getIconForZone";

export default class WidgetZoneActivityState {

private static instance: WidgetZoneActivityState | null = null;

widget: unknown;

private constructor(private homey: Homey, private homeyApi: ExtendedHomeyAPIV3Local, private zonesDb: ZonesDb, private log: (...args: unknown[]) => void, private error: (...args: unknown[]) => void) {
// @ts-expect-error Ignore the error for the non-existing method / property
this.widget = this.homey.dashboards.getWidget('zone-activity-state');
}

public static async initialize(homey: Homey, homeyApi: ExtendedHomeyAPIV3Local, zonesDb: ZonesDb, log: (...args: unknown[]) => void, error: (...args: unknown[]) => void): Promise<WidgetZoneActivityState> {
if (WidgetZoneActivityState.instance === null) {
WidgetZoneActivityState.instance = new WidgetZoneActivityState(homey, homeyApi, zonesDb, log, error);
await WidgetZoneActivityState.instance.setup();
}
return WidgetZoneActivityState.instance;
}

private async setup(): Promise<void> {
// @ts-expect-error Ignore the error for the non-existing method / property
this.widget.registerSettingAutocompleteListener('zone', async (query: string) => {
const result = (await handleZoneAutocomplete(query, this.zonesDb))
// Remap icon to image for all results
.map((zone) => {
zone.image = zone.icon;
return zone;
});
return result;
});

this.homeyApi.zones.on('zone.update', async (zone: ExtendedZone) => {
const data = await this.transformZone(zone);
this.homey.api.realtime(`zone-update-${zone.id}`, data);
});
}

public async getZone(zoneId: string): Promise<unknown> {
const zone = await this.zonesDb.getZone(zoneId);
if (zone === null)
return null;

return await this.transformZone(zone);
}

private async transformZone(zone: ExtendedZone): Promise<unknown> {
const result = {
id: zone.id,
iconSrc: await getZoneImageSource(zone.icon),
icon: getIconForZone(zone.icon),
name: zone.name,
active: zone.active,
activeLastUpdated: zone.activeLastUpdated,
}

return result;
}
}

12 changes: 11 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{
"any_type": "Any type",
"any_sensor": "Any sensor"
"any_sensor": "Any sensor",
"active": "Active",
"last_active": "Last active",
"hour_ago": "hour ago",
"hours_ago": "hours ago",
"minute_ago": "minute ago",
"minutes_ago": "minutes ago",
"second_ago": "second ago",
"seconds_ago": "seconds ago",
"widget_error": "An error occurred",
"contact_dev": "Please contact the developer"
}
12 changes: 11 additions & 1 deletion locales/nl.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{
"any_type": "Elk type",
"any_sensor": "Elke sensor"
"any_sensor": "Elke sensor",
"active": "Actief",
"last_active": "Laatst actief",
"hours_ago": "uur geleden",
"hour_ago": "uur geleden",
"minutes_ago": "minuten geleden",
"minute_ago": "minuut geleden",
"second_ago": "seconde geleden",
"seconds_ago": "seconden geleden",
"widget_error": "Er is een fout is opgetreden",
"contact_dev": "Neem contact op met de ontwikkelaar"
}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"devDependencies": {
"@eslint-recommended/eslint-config": "^26.1.1",
"@tsconfig/node16": "^16.1.3",
"@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.7",
"@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.9",
"@types/node": "^22.5.1",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"sourceMap": true
},
"include": [
"./**/*.ts"
"./**/*.ts",
"./widgets/zone-activity-state/api.js"
],
"exclude": [
"node_modules",
Expand Down
22 changes: 22 additions & 0 deletions utils/getIconForZone.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
const cache: { [key: string]: string } = {};

function getZoneImagePath(t: string): string {
return `https://my.homey.app/img/zones/${t}`;
}

async function getZoneImageSource(iconName: string): Promise<string | null> {
if (cache[iconName]) {
return cache[iconName];
}
const url = getIconForZone(iconName);
try {
const response = await fetch(url);
if (!response.ok || !response.headers.get('content-type')?.includes('image/svg+xml')) {
throw new Error('Invalid response while fetching icon');
}
const svgSource = await response.text();
cache[iconName] = svgSource;
return svgSource;
} catch (_error) {
return null;
}
}

export default function getIconForZone(iconName: string): string {
switch (iconName) {
case 'firstFloor':
Expand Down Expand Up @@ -108,3 +128,5 @@ export default function getIconForZone(iconName: string): string {
return getZoneImagePath('hallway_door.svg');
}
}

export { getZoneImageSource };
21 changes: 21 additions & 0 deletions widgets/zone-activity-state/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

import Homey from "homey/lib/Homey";

class ZoneActivityStateWidget {
public async getZone({ homey, query }: ApiRequest) : Promise<string> {
//@ts-expect-error: Ignore the error for the non-existing method
const result = await homey.app.widget.getZone(query.zoneId);
homey.app.log('Widget initialized,', { widgetInstanceId: query.widgetInstanceId, zoneId: query.zoneId, result });
return result;
}

public async log({ homey, body }: ApiRequest) : Promise<void> {
const message = `[${this.constructor.name}]: ${body[0]}`;
const args = body.slice(1);
homey.app.log(message, ...args);
}
}

module.exports = new ZoneActivityStateWidget();

type ApiRequest = { homey: Homey; query: any, body: any, params: unknown };

Check warning on line 21 in widgets/zone-activity-state/api.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type

Check warning on line 21 in widgets/zone-activity-state/api.ts

View workflow job for this annotation

GitHub Actions / eslint

Unexpected any. Specify a different type
Binary file added widgets/zone-activity-state/preview-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added widgets/zone-activity-state/preview-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions widgets/zone-activity-state/public/error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 30e5758

Please sign in to comment.