From e7edb88b7982effe2ab2c1635389663abe5e41fa Mon Sep 17 00:00:00 2001 From: regnerus Date: Tue, 12 May 2026 17:44:33 +0200 Subject: [PATCH 1/3] feat(zone): add zone icon API --- assets/zone/icons.json | 68 +++++++++++++++++++++++++++++++ index.js | 7 ++++ lib/Zone/index.js | 90 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 assets/zone/icons.json create mode 100644 lib/Zone/index.js diff --git a/assets/zone/icons.json b/assets/zone/icons.json new file mode 100644 index 00000000..854240d6 --- /dev/null +++ b/assets/zone/icons.json @@ -0,0 +1,68 @@ +{ + "defaultIconId": "default", + "icons": [ + { "id": "firstFloor", "fileName": "first-floor.svg" }, + { "id": "groundFloor", "fileName": "ground-floor.svg" }, + { "id": "officeBuildings", "fileName": "office-buildings.svg" }, + { "id": "singleFloor", "fileName": "single-floor.svg" }, + { "id": "topFloor", "fileName": "top-floor.svg" }, + { "id": "default", "fileName": "zone.svg" }, + { "id": "home", "fileName": "home.svg" }, + { "id": "floor", "fileName": "floor.svg" }, + { "id": "basement", "fileName": "basement.svg" }, + { "id": "attic", "fileName": "attic.svg" }, + { "id": "roofSolar", "fileName": "roof_solar_panels.svg" }, + { "id": "garage", "fileName": "garage.svg" }, + { "id": "pool", "fileName": "pool.svg" }, + { "id": "garden", "fileName": "garden.svg" }, + { "id": "gardenShed", "fileName": "garden_shed.svg" }, + { "id": "terrace", "fileName": "terrace.svg" }, + { "id": "carport", "fileName": "carport.svg" }, + { "id": "hallway", "fileName": "hallway.svg" }, + { "id": "livingRoom", "fileName": "living_room.svg" }, + { "id": "kitchen", "fileName": "kitchen.svg" }, + { "id": "diningRoom", "fileName": "dining_room.svg" }, + { "id": "bedroomSingle", "fileName": "bedroom.svg" }, + { "id": "bedroomDouble", "fileName": "master_bedroom.svg" }, + { "id": "bedroomKids", "fileName": "kinder_room.svg" }, + { "id": "bathroom", "fileName": "bathroom.svg" }, + { "id": "wardrobe", "fileName": "wardrobe.svg" }, + { "id": "office", "fileName": "office.svg" }, + { "id": "gym", "fileName": "gym.svg" }, + { "id": "toilet", "fileName": "toilet.svg" }, + { "id": "entrance", "fileName": "entrance_front_door.svg" }, + { "id": "studyRoom", "fileName": "study_room.svg" }, + { "id": "gameRoom", "fileName": "game_room.svg" }, + { "id": "utilitiesRoom", "fileName": "utilities_room.svg" }, + { "id": "recreationRoom", "fileName": "recreation_room.svg" }, + { "id": "tipi", "fileName": "tipi_tent.svg" }, + { "id": "doorClosed", "fileName": "door-closed.svg" }, + { "id": "lounge", "fileName": "lounge-chair.svg" }, + { "id": "fuseBox", "fileName": "lightning-bolt.svg" }, + { "id": "laundryRoom", "fileName": "laundry-basket.svg" }, + { "id": "phone", "fileName": "phone.svg" }, + { "id": "coffeeMachine", "fileName": "coffee-machine.svg" }, + { "id": "drinks", "fileName": "drinks.svg" }, + { "id": "pantry", "fileName": "pantry.svg" }, + { "id": "suitcase", "fileName": "suitcase.svg" }, + { "id": "door", "fileName": "door.svg" }, + { "id": "toothbrush", "fileName": "toothbrush.svg" }, + { "id": "sink", "fileName": "sink.svg" } + ], + "legacyIcons": [ + { "id": "bed", "targetId": "bedroomDouble" }, + { "id": "bedroom", "targetId": "bedroomSingle" }, + { "id": "books", "targetId": "studyRoom" }, + { "id": "entranceFrontDoor", "targetId": "entrance" }, + { "id": "hallwayDoor", "targetId": "hallwayDoor", "fileName": "hallway_door.svg" }, + { "id": "kinderRoom", "targetId": "bedroomKids" }, + { "id": "living", "targetId": "livingRoom" }, + { "id": "masterBedroom", "targetId": "bedroomDouble" }, + { "id": "roof", "targetId": "attic" }, + { "id": "roofSolarPanels", "targetId": "roofSolar" }, + { "id": "shower", "targetId": "bathroom" }, + { "id": "stairs-down", "targetId": "basement" }, + { "id": "stairs-up", "targetId": "floor" }, + { "id": "tipiTent", "targetId": "tipi" } + ] +} diff --git a/index.js b/index.js index aa92603e..09e09a41 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const Energy = require('./lib/Energy'); const Media = require('./lib/Media'); const Signal = require('./lib/Signal'); const Util = require('./lib/Util'); +const Zone = require('./lib/Zone'); module.exports.App = App; module.exports.Capability = Capability; @@ -15,6 +16,7 @@ module.exports.Energy = Energy; module.exports.Media = Media; module.exports.Signal = Signal; module.exports.Util = Util; +module.exports.Zone = Zone; /** @type {typeof Device.getClasses} */ module.exports.getDeviceClasses = Device.getClasses.bind(Device); @@ -45,6 +47,11 @@ module.exports.getCurrencies = Energy.getCurrencies.bind(Energy); /** @type {typeof Energy.getBatteries} */ module.exports.getBatteries = Energy.getBatteries.bind(Energy); +/** @type {typeof Zone.getIcons} */ +module.exports.getZoneIcons = Zone.getIcons.bind(Zone); +/** @type {typeof Zone.resolveIcon} */ +module.exports.resolveZoneIcon = Zone.resolveIcon.bind(Zone); + /** @typedef {import('./assets/app/schema').App} AppManifest */ /** @typedef {import('./assets/capability/schema').Capability} CapabilityDefinition */ /** @typedef {import('./assets/app/schema').ZigbeeFirmwareUpdates} ZigbeeFirmwareUpdates */ diff --git a/lib/Zone/index.js b/lib/Zone/index.js new file mode 100644 index 00000000..425c7007 --- /dev/null +++ b/lib/Zone/index.js @@ -0,0 +1,90 @@ +'use strict'; + +const { join } = require('../../helpers'); + +/** @typedef {{ id: string, fileName: string }} ZoneIconManifestIcon */ +/** @typedef {{ id: string, targetId: string, fileName?: string }} ZoneIconManifestLegacyIcon */ +/** @typedef {{ defaultIconId: string, icons: ZoneIconManifestIcon[], legacyIcons: ZoneIconManifestLegacyIcon[] }} ZoneIconManifest */ +/** @typedef {{ id: string, fileName: string, path?: string }} ZoneIconDescriptor */ + +/** @type {ZoneIconManifest} */ +const ICONS_MANIFEST = require('../../assets/zone/icons.json'); + +const ICONS_PATH_SEGMENTS = [__dirname, '..', '..', 'assets', 'zone', 'icons']; +const DEFAULT_ICON_ID = ICONS_MANIFEST.defaultIconId; +const ACTIVE_ICONS = ICONS_MANIFEST.icons.reduce((obj, icon) => { + obj[icon.id] = icon.fileName; + return obj; +}, {}); +const LEGACY_ICONS = ICONS_MANIFEST.legacyIcons.reduce((obj, icon) => { + obj[icon.id] = { + id: icon.targetId, + fileName: icon.fileName, + }; + return obj; +}, {}); + +/** @type {ZoneIconDescriptor[] | undefined} */ +let iconsCache; + +class Zone { + + /** + * @returns {ZoneIconDescriptor[]} + */ + static getIcons() { + if (iconsCache) return iconsCache; + + iconsCache = ICONS_MANIFEST.icons.map(({ id, fileName }) => Zone._createIconDescriptor({ + id, + fileName, + })); + + return iconsCache; + } + + /** + * @param {string | null | undefined} icon + * @returns {ZoneIconDescriptor} + */ + static resolveIcon(icon) { + const legacyIcon = typeof icon === 'string' ? LEGACY_ICONS[icon] : undefined; + const id = legacyIcon ? legacyIcon.id : icon; + let fileName = legacyIcon ? legacyIcon.fileName : undefined; + + if (!fileName && typeof id === 'string') { + fileName = ACTIVE_ICONS[id]; + } + + if (!fileName) { + return Zone._createIconDescriptor({ + id: DEFAULT_ICON_ID, + fileName: ACTIVE_ICONS[DEFAULT_ICON_ID], + }); + } + + return Zone._createIconDescriptor({ + id, + fileName, + }); + } + + static _createIconDescriptor({ + id, + fileName, + }) { + const descriptor = { + id, + fileName, + }; + + if (typeof join === 'function') { + descriptor.path = join(...ICONS_PATH_SEGMENTS, fileName); + } + + return descriptor; + } + +} + +module.exports = Zone; From 88ee8f040f56566b3eed204559b70c23d597770b Mon Sep 17 00:00:00 2001 From: regnerus Date: Tue, 12 May 2026 17:44:44 +0200 Subject: [PATCH 2/3] test(zone): cover zone icon API --- test/zone.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/zone.js diff --git a/test/zone.js b/test/zone.js new file mode 100644 index 00000000..02a70a89 --- /dev/null +++ b/test/zone.js @@ -0,0 +1,31 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); + +const HomeyLib = require('..'); + +describe('Zone icons', function() { + it('returns active zone icons', function() { + const icons = HomeyLib.getZoneIcons(); + + assert.strictEqual(icons.length, 47); + assert.strictEqual(icons[0].id, 'firstFloor'); + assert.strictEqual(icons[0].fileName, 'first-floor.svg'); + assert.strictEqual(icons.some(icon => icon.id === 'hallwayDoor'), false); + + for (const icon of icons) { + assert.strictEqual(typeof icon.id, 'string'); + assert.strictEqual(typeof icon.fileName, 'string'); + assert.strictEqual(typeof icon.path, 'string'); + assert.strictEqual(fs.existsSync(icon.path), true); + } + }); + + it('resolves active, legacy, and unknown zone icons', function() { + assert.strictEqual(HomeyLib.resolveZoneIcon('default').fileName, 'zone.svg'); + assert.strictEqual(HomeyLib.resolveZoneIcon('bed').id, 'bedroomDouble'); + assert.strictEqual(HomeyLib.resolveZoneIcon('hallwayDoor').fileName, 'hallway_door.svg'); + assert.strictEqual(HomeyLib.resolveZoneIcon('unknown').id, 'default'); + }); +}); From 74a9333ecdd5259e388832d5a9300602014dbe83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 13 May 2026 06:26:16 +0000 Subject: [PATCH 3/3] fix(zone): use hyphenated zone svg filenames Agent-Logs-Url: https://github.com/athombv/node-homey-lib/sessions/80694955-b0c5-4d6e-ad42-bafe6aa2d036 Co-authored-by: HarwinBorger <3984675+HarwinBorger@users.noreply.github.com> --- assets/zone/icons.json | 26 +++++++++---------- .../{dining_room.svg => dining-room.svg} | 0 ...front_door.svg => entrance-front-door.svg} | 0 .../icons/{game_room.svg => game-room.svg} | 0 .../{garden_shed.svg => garden-shed.svg} | 0 .../{hallway_door.svg => hallway-door.svg} | 0 .../{kinder_room.svg => kinder-room.svg} | 0 .../{living_room.svg => living-room.svg} | 0 ...{master_bedroom.svg => master-bedroom.svg} | 0 ...ecreation_room.svg => recreation-room.svg} | 0 ...solar_panels.svg => roof-solar-panels.svg} | 0 .../icons/{study_room.svg => study-room.svg} | 0 .../icons/{tipi_tent.svg => tipi-tent.svg} | 0 ...{utilities_room.svg => utilities-room.svg} | 0 test/zone.js | 2 +- 15 files changed, 14 insertions(+), 14 deletions(-) rename assets/zone/icons/{dining_room.svg => dining-room.svg} (100%) rename assets/zone/icons/{entrance_front_door.svg => entrance-front-door.svg} (100%) rename assets/zone/icons/{game_room.svg => game-room.svg} (100%) rename assets/zone/icons/{garden_shed.svg => garden-shed.svg} (100%) rename assets/zone/icons/{hallway_door.svg => hallway-door.svg} (100%) rename assets/zone/icons/{kinder_room.svg => kinder-room.svg} (100%) rename assets/zone/icons/{living_room.svg => living-room.svg} (100%) rename assets/zone/icons/{master_bedroom.svg => master-bedroom.svg} (100%) rename assets/zone/icons/{recreation_room.svg => recreation-room.svg} (100%) rename assets/zone/icons/{roof_solar_panels.svg => roof-solar-panels.svg} (100%) rename assets/zone/icons/{study_room.svg => study-room.svg} (100%) rename assets/zone/icons/{tipi_tent.svg => tipi-tent.svg} (100%) rename assets/zone/icons/{utilities_room.svg => utilities-room.svg} (100%) diff --git a/assets/zone/icons.json b/assets/zone/icons.json index 854240d6..ba2bfd20 100644 --- a/assets/zone/icons.json +++ b/assets/zone/icons.json @@ -11,31 +11,31 @@ { "id": "floor", "fileName": "floor.svg" }, { "id": "basement", "fileName": "basement.svg" }, { "id": "attic", "fileName": "attic.svg" }, - { "id": "roofSolar", "fileName": "roof_solar_panels.svg" }, + { "id": "roofSolar", "fileName": "roof-solar-panels.svg" }, { "id": "garage", "fileName": "garage.svg" }, { "id": "pool", "fileName": "pool.svg" }, { "id": "garden", "fileName": "garden.svg" }, - { "id": "gardenShed", "fileName": "garden_shed.svg" }, + { "id": "gardenShed", "fileName": "garden-shed.svg" }, { "id": "terrace", "fileName": "terrace.svg" }, { "id": "carport", "fileName": "carport.svg" }, { "id": "hallway", "fileName": "hallway.svg" }, - { "id": "livingRoom", "fileName": "living_room.svg" }, + { "id": "livingRoom", "fileName": "living-room.svg" }, { "id": "kitchen", "fileName": "kitchen.svg" }, - { "id": "diningRoom", "fileName": "dining_room.svg" }, + { "id": "diningRoom", "fileName": "dining-room.svg" }, { "id": "bedroomSingle", "fileName": "bedroom.svg" }, - { "id": "bedroomDouble", "fileName": "master_bedroom.svg" }, - { "id": "bedroomKids", "fileName": "kinder_room.svg" }, + { "id": "bedroomDouble", "fileName": "master-bedroom.svg" }, + { "id": "bedroomKids", "fileName": "kinder-room.svg" }, { "id": "bathroom", "fileName": "bathroom.svg" }, { "id": "wardrobe", "fileName": "wardrobe.svg" }, { "id": "office", "fileName": "office.svg" }, { "id": "gym", "fileName": "gym.svg" }, { "id": "toilet", "fileName": "toilet.svg" }, - { "id": "entrance", "fileName": "entrance_front_door.svg" }, - { "id": "studyRoom", "fileName": "study_room.svg" }, - { "id": "gameRoom", "fileName": "game_room.svg" }, - { "id": "utilitiesRoom", "fileName": "utilities_room.svg" }, - { "id": "recreationRoom", "fileName": "recreation_room.svg" }, - { "id": "tipi", "fileName": "tipi_tent.svg" }, + { "id": "entrance", "fileName": "entrance-front-door.svg" }, + { "id": "studyRoom", "fileName": "study-room.svg" }, + { "id": "gameRoom", "fileName": "game-room.svg" }, + { "id": "utilitiesRoom", "fileName": "utilities-room.svg" }, + { "id": "recreationRoom", "fileName": "recreation-room.svg" }, + { "id": "tipi", "fileName": "tipi-tent.svg" }, { "id": "doorClosed", "fileName": "door-closed.svg" }, { "id": "lounge", "fileName": "lounge-chair.svg" }, { "id": "fuseBox", "fileName": "lightning-bolt.svg" }, @@ -54,7 +54,7 @@ { "id": "bedroom", "targetId": "bedroomSingle" }, { "id": "books", "targetId": "studyRoom" }, { "id": "entranceFrontDoor", "targetId": "entrance" }, - { "id": "hallwayDoor", "targetId": "hallwayDoor", "fileName": "hallway_door.svg" }, + { "id": "hallwayDoor", "targetId": "hallwayDoor", "fileName": "hallway-door.svg" }, { "id": "kinderRoom", "targetId": "bedroomKids" }, { "id": "living", "targetId": "livingRoom" }, { "id": "masterBedroom", "targetId": "bedroomDouble" }, diff --git a/assets/zone/icons/dining_room.svg b/assets/zone/icons/dining-room.svg similarity index 100% rename from assets/zone/icons/dining_room.svg rename to assets/zone/icons/dining-room.svg diff --git a/assets/zone/icons/entrance_front_door.svg b/assets/zone/icons/entrance-front-door.svg similarity index 100% rename from assets/zone/icons/entrance_front_door.svg rename to assets/zone/icons/entrance-front-door.svg diff --git a/assets/zone/icons/game_room.svg b/assets/zone/icons/game-room.svg similarity index 100% rename from assets/zone/icons/game_room.svg rename to assets/zone/icons/game-room.svg diff --git a/assets/zone/icons/garden_shed.svg b/assets/zone/icons/garden-shed.svg similarity index 100% rename from assets/zone/icons/garden_shed.svg rename to assets/zone/icons/garden-shed.svg diff --git a/assets/zone/icons/hallway_door.svg b/assets/zone/icons/hallway-door.svg similarity index 100% rename from assets/zone/icons/hallway_door.svg rename to assets/zone/icons/hallway-door.svg diff --git a/assets/zone/icons/kinder_room.svg b/assets/zone/icons/kinder-room.svg similarity index 100% rename from assets/zone/icons/kinder_room.svg rename to assets/zone/icons/kinder-room.svg diff --git a/assets/zone/icons/living_room.svg b/assets/zone/icons/living-room.svg similarity index 100% rename from assets/zone/icons/living_room.svg rename to assets/zone/icons/living-room.svg diff --git a/assets/zone/icons/master_bedroom.svg b/assets/zone/icons/master-bedroom.svg similarity index 100% rename from assets/zone/icons/master_bedroom.svg rename to assets/zone/icons/master-bedroom.svg diff --git a/assets/zone/icons/recreation_room.svg b/assets/zone/icons/recreation-room.svg similarity index 100% rename from assets/zone/icons/recreation_room.svg rename to assets/zone/icons/recreation-room.svg diff --git a/assets/zone/icons/roof_solar_panels.svg b/assets/zone/icons/roof-solar-panels.svg similarity index 100% rename from assets/zone/icons/roof_solar_panels.svg rename to assets/zone/icons/roof-solar-panels.svg diff --git a/assets/zone/icons/study_room.svg b/assets/zone/icons/study-room.svg similarity index 100% rename from assets/zone/icons/study_room.svg rename to assets/zone/icons/study-room.svg diff --git a/assets/zone/icons/tipi_tent.svg b/assets/zone/icons/tipi-tent.svg similarity index 100% rename from assets/zone/icons/tipi_tent.svg rename to assets/zone/icons/tipi-tent.svg diff --git a/assets/zone/icons/utilities_room.svg b/assets/zone/icons/utilities-room.svg similarity index 100% rename from assets/zone/icons/utilities_room.svg rename to assets/zone/icons/utilities-room.svg diff --git a/test/zone.js b/test/zone.js index 02a70a89..dc4c153b 100644 --- a/test/zone.js +++ b/test/zone.js @@ -25,7 +25,7 @@ describe('Zone icons', function() { it('resolves active, legacy, and unknown zone icons', function() { assert.strictEqual(HomeyLib.resolveZoneIcon('default').fileName, 'zone.svg'); assert.strictEqual(HomeyLib.resolveZoneIcon('bed').id, 'bedroomDouble'); - assert.strictEqual(HomeyLib.resolveZoneIcon('hallwayDoor').fileName, 'hallway_door.svg'); + assert.strictEqual(HomeyLib.resolveZoneIcon('hallwayDoor').fileName, 'hallway-door.svg'); assert.strictEqual(HomeyLib.resolveZoneIcon('unknown').id, 'default'); }); });