From 2b546c3749b80a2cee986e85a80acd90c36868c6 Mon Sep 17 00:00:00 2001 From: Olivier Zalmanski <88216225+OlivierZal@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:25:08 +0200 Subject: [PATCH] First widget (#526) --- .homeycompose/locales/da.json | 4 +- .homeycompose/locales/en.json | 4 +- .homeycompose/locales/es.json | 4 +- .homeycompose/locales/fr.json | 6 +- .homeycompose/locales/nl.json | 4 +- .homeycompose/locales/no.json | 4 +- .homeycompose/locales/sv.json | 4 +- api.ts | 310 ++------------ app.json | 34 ++ app.ts | 332 +++++++++++++-- bases/device.ts | 39 +- bases/driver.ts | 6 +- drivers/melcloud/device.ts | 4 +- drivers/melcloud_atw/device.ts | 12 +- drivers/melcloud_atw/driver.ts | 6 +- drivers/melcloud_erv/device.ts | 4 +- drivers/melcloud_erv/driver.ts | 4 +- eslint.config.mjs | 39 +- lib/getBuildings.ts | 41 ++ lib/index.ts | 1 + locales/da.json | 9 +- locales/en.json | 9 +- locales/es.json | 9 +- locales/fr.json | 6 +- locales/nl.json | 9 +- locales/no.json | 9 +- locales/sv.json | 9 +- mixins/withTimers.ts | 4 +- package-lock.json | 8 +- package.json | 2 +- settings/index.html | 2 +- settings/index.ts | 12 +- types/ata.ts | 1 + types/atw.ts | 5 +- types/common.ts | 15 +- types/erv.ts | 1 + types/index.ts | 32 +- widgets/ata-group-setting/api.ts | 46 ++ widgets/ata-group-setting/preview-dark.png | Bin 0 -> 161716 bytes widgets/ata-group-setting/preview-light.png | Bin 0 -> 161157 bytes widgets/ata-group-setting/public/index.html | 34 ++ widgets/ata-group-setting/public/index.ts | 397 ++++++++++++++++++ widgets/ata-group-setting/public/styles.css | 11 + widgets/ata-group-setting/widget.compose.json | 31 ++ 44 files changed, 1097 insertions(+), 426 deletions(-) create mode 100644 lib/getBuildings.ts create mode 100644 lib/index.ts create mode 100644 widgets/ata-group-setting/api.ts create mode 100644 widgets/ata-group-setting/preview-dark.png create mode 100644 widgets/ata-group-setting/preview-light.png create mode 100644 widgets/ata-group-setting/public/index.html create mode 100644 widgets/ata-group-setting/public/index.ts create mode 100644 widgets/ata-group-setting/public/styles.css create mode 100644 widgets/ata-group-setting/widget.compose.json diff --git a/.homeycompose/locales/da.json b/.homeycompose/locales/da.json index 0dde9023..8b6d4525 100644 --- a/.homeycompose/locales/da.json +++ b/.homeycompose/locales/da.json @@ -1,5 +1,6 @@ { "errors": { + "deviceNotFound": "Enhed ikke fundet.", "zoneNotFound": "Zone ikke fundet." }, "settings": { @@ -68,7 +69,6 @@ "title": "MELCloud-indstillinger" }, "warnings": { - "dashboard": "Forlad enheden og vend tilbage for at opdatere dit instrumentbræt.", - "deviceNotFound": "Enhed ikke fundet." + "dashboard": "Forlad enheden og vend tilbage for at opdatere dit instrumentbræt." } } diff --git a/.homeycompose/locales/en.json b/.homeycompose/locales/en.json index 104f4df2..5efe8211 100644 --- a/.homeycompose/locales/en.json +++ b/.homeycompose/locales/en.json @@ -1,5 +1,6 @@ { "errors": { + "deviceNotFound": "Device not found.", "zoneNotFound": "Zone not found." }, "settings": { @@ -68,7 +69,6 @@ "title": "MELCloud settings" }, "warnings": { - "dashboard": "Exit the device and return to update your dashboard.", - "deviceNotFound": "Device not found." + "dashboard": "Exit the device and return to update your dashboard." } } diff --git a/.homeycompose/locales/es.json b/.homeycompose/locales/es.json index 17eee46d..1fced862 100644 --- a/.homeycompose/locales/es.json +++ b/.homeycompose/locales/es.json @@ -1,5 +1,6 @@ { "errors": { + "deviceNotFound": "Dispositivo no encontrada.", "zoneNotFound": "Zona no encontrada." }, "settings": { @@ -68,7 +69,6 @@ "title": "Configuración de MELCloud" }, "warnings": { - "dashboard": "Salga del dispositivo y regrese para actualizar su panel de control.", - "deviceNotFound": "Dispositivo no encontrada." + "dashboard": "Salga del dispositivo y regrese para actualizar su panel de control." } } diff --git a/.homeycompose/locales/fr.json b/.homeycompose/locales/fr.json index 70ba9e58..6dde7619 100644 --- a/.homeycompose/locales/fr.json +++ b/.homeycompose/locales/fr.json @@ -1,6 +1,7 @@ { "errors": { - "zoneNotFound": "La zone est introuvable." + "deviceNotFound": "Appareil introuvable.", + "zoneNotFound": "Zone introuvable." }, "settings": { "authenticate": { @@ -68,7 +69,6 @@ "title": "Paramètres MELCloud" }, "warnings": { - "dashboard": "Sortez de l'appareil et revenez pour mettre à jour votre tableau de bord.", - "deviceNotFound": "L'appareil est introuvable." + "dashboard": "Sortez de l'appareil et revenez pour mettre à jour votre tableau de bord." } } diff --git a/.homeycompose/locales/nl.json b/.homeycompose/locales/nl.json index 27ee082f..8520e69a 100644 --- a/.homeycompose/locales/nl.json +++ b/.homeycompose/locales/nl.json @@ -1,5 +1,6 @@ { "errors": { + "deviceNotFound": "Apparaat niet gevonden.", "zoneNotFound": "Zone niet gevonden." }, "settings": { @@ -68,7 +69,6 @@ "title": "MELCloud-instellingen" }, "warnings": { - "dashboard": "Verlaat het apparaat en keer terug om uw dashboard te updaten.", - "deviceNotFound": "Apparaat niet gevonden." + "dashboard": "Verlaat het apparaat en keer terug om uw dashboard te updaten." } } diff --git a/.homeycompose/locales/no.json b/.homeycompose/locales/no.json index c185d31b..6ecfc096 100644 --- a/.homeycompose/locales/no.json +++ b/.homeycompose/locales/no.json @@ -1,5 +1,6 @@ { "errors": { + "deviceNotFound": "Enhet ikke funnet.", "zoneNotFound": "Sone ikke funnet." }, "settings": { @@ -68,7 +69,6 @@ "title": "MELCloud-innstillinger" }, "warnings": { - "dashboard": "Avslutt enheten og gå tilbake for å oppdatere dashbordet ditt.", - "deviceNotFound": "Enhet ikke funnet." + "dashboard": "Avslutt enheten og gå tilbake for å oppdatere dashbordet ditt." } } diff --git a/.homeycompose/locales/sv.json b/.homeycompose/locales/sv.json index b3815f03..1f92ee6f 100644 --- a/.homeycompose/locales/sv.json +++ b/.homeycompose/locales/sv.json @@ -1,5 +1,6 @@ { "errors": { + "deviceNotFound": "Enhet ej hittad.", "zoneNotFound": "Zon ej hittad." }, "settings": { @@ -68,7 +69,6 @@ "title": "MELCloud-inställningar" }, "warnings": { - "dashboard": "Avsluta enheten och återvänd för att uppdatera din instrumentpanel.", - "deviceNotFound": "Enhet ej hittad." + "dashboard": "Avsluta enheten och återvänd för att uppdatera din instrumentpanel." } } diff --git a/api.ts b/api.ts index eab588c2..6717be24 100644 --- a/api.ts +++ b/api.ts @@ -1,174 +1,29 @@ -import type Homey from 'homey/lib/Homey' +import { getBuildings } from './lib' -import { - type AreaFacade, - type AreaModelAny, - type BuildingFacade, - type FloorFacade, - type FloorModel, - type FrostProtectionData, - type GroupAtaState, - type HolidayModeData, - type LoginCredentials, - BuildingModel, - FanSpeed, - Horizontal, - OperationMode, - Vertical, +import type { + FrostProtectionData, + GroupAtaState, + HolidayModeData, + LoginCredentials, } from '@olivierzal/melcloud-api' -import fanSpeed from 'homey-lib/assets/capability/capabilities/fan_speed.json' -import power from 'homey-lib/assets/capability/capabilities/onoff.json' -import setTemperature from 'homey-lib/assets/capability/capabilities/target_temperature.json' -import thermostatMode from 'homey-lib/assets/capability/capabilities/thermostat_mode.json' - -import type MELCloudApp from '.' +import type Homey from 'homey/lib/Homey' -import horizontal from './.homeycompose/capabilities/horizontal.json' -import vertical from './.homeycompose/capabilities/vertical.json' -import { - type AreaZone, - type BuildingZone, - type DeviceSettings, - type DriverCapabilitiesOptions, - type DriverSetting, - type ErrorLog, - type ErrorLogQuery, - type FloorZone, - type FrostProtectionSettings, - type HolidayModeSettings, - type LoginSetting, - type Manifest, - type ManifestDriver, - type ManifestDriverCapabilitiesOptions, - type Settings, - type ZoneData, - fanSpeedValues, - modelClass, +import type { + BuildingZone, + DeviceSettings, + DriverCapabilitiesOptions, + DriverSetting, + ErrorLog, + ErrorLogQuery, + FrostProtectionSettings, + HolidayModeSettings, + Settings, + ZoneData, } from './types' -const compareNames = ( - { name: name1 }: { name: string }, - { name: name2 }: { name: string }, -): number => name1.localeCompare(name2) - -const mapArea = ({ id, name }: AreaModelAny): AreaZone => ({ - id, - name, -}) - -const mapFloor = ({ areas, id, name }: FloorModel): FloorZone => ({ - areas: areas.sort(compareNames).map(mapArea), - id, - name, -}) - -const mapBuilding = ({ - areas, - floors, - id, - name, -}: BuildingModel): BuildingZone => ({ - areas: areas - .filter(({ floorId }: { floorId: number | null }) => floorId === null) - .sort(compareNames) - .map(mapArea), - floors: floors.sort(compareNames).map(mapFloor), - id, - name, -}) - -const getFacade = ( - homey: Homey, - zoneType: keyof typeof modelClass, - id: number, -): AreaFacade | BuildingFacade | FloorFacade => { - const model = modelClass[zoneType].getById(id) - if (!model) { - throw new Error(homey.__('errors.zoneNotFound')) - } - return (homey.app as MELCloudApp).facadeManager.get(model) -} - -const formatErrors = (errors: Record): string => - Object.entries(errors) - .map(([error, messages]) => `${error}: ${messages.join(', ')}`) - .join('\n') - -const handleResponse = ( - errors: Record | null, -): void => { - if (errors) { - throw new Error(formatErrors(errors)) - } -} - -const getDriverSettings = ( - { id: driverId, settings }: ManifestDriver, - language: string, -): DriverSetting[] => - (settings ?? []).flatMap(({ children, id: groupId, label: groupLabel }) => - (children ?? []).map(({ id, label, max, min, type, units, values }) => ({ - driverId, - groupId, - groupLabel: groupLabel[language] ?? groupLabel.en, - id, - max, - min, - title: label[language] ?? label.en, - type, - units, - values: values?.map(({ id: valueId, label: valueLabel }) => ({ - id: valueId, - label: valueLabel[language] ?? valueLabel.en, - })), - })), - ) - -const getDriverLoginSetting = ( - { id: driverId, pair }: ManifestDriver, - language: string, -): DriverSetting[] => - Object.values( - Object.entries( - pair?.find( - (pairSetting): pairSetting is LoginSetting => - pairSetting.id === 'login', - )?.options ?? [], - ).reduce>((acc, [option, label]) => { - const isPassword = option.startsWith('password') - const key = isPassword ? 'password' : 'username' - acc[key] ??= { - driverId, - groupId: 'login', - id: key, - title: '', - type: isPassword ? 'password' : 'text', - } - acc[key][option.endsWith('Placeholder') ? 'placeholder' : 'title'] = - label[language] ?? label.en - return acc - }, {}), - ) +import type MELCloudApp from '.' -const getLocalizedCapabilitiesOptions = ( - options: ManifestDriverCapabilitiesOptions, - language: string, - enumType?: - | typeof FanSpeed - | typeof Horizontal - | typeof OperationMode - | typeof Vertical, -): DriverCapabilitiesOptions => ({ - title: options.title[language] ?? options.title.en, - type: options.type, - values: options.values?.map(({ id, title }) => ({ - id: - enumType && id in enumType ? - String(enumType[id as keyof typeof enumType]) - : id, - label: title[language] ?? title.en, - })), -}) +const getApp = (homey: Homey): MELCloudApp => homey.app as MELCloudApp export = { getAtaCapabilities({ @@ -176,80 +31,29 @@ export = { }: { homey: Homey }): [keyof GroupAtaState, DriverCapabilitiesOptions][] { - const language = homey.i18n.getLanguage() - return [ - { key: 'Power', options: power }, - { key: 'SetTemperature', options: setTemperature }, - { - enumType: FanSpeed, - key: 'FanSpeed', - options: { ...fanSpeed, type: 'enum', values: fanSpeedValues }, - }, - { enumType: Vertical, key: 'VaneVerticalDirection', options: vertical }, - { - enumType: Horizontal, - key: 'VaneHorizontalDirection', - options: horizontal, - }, - { - enumType: OperationMode, - key: 'OperationMode', - options: { - ...thermostatMode, - values: (homey.manifest as Manifest).drivers - .find(({ id }) => id === 'melcloud') - ?.capabilitiesOptions?.thermostat_mode.values?.filter( - ({ id }) => id !== 'off', - ), - }, - }, - ].map(({ enumType, key, options }) => [ - key as keyof GroupAtaState, - getLocalizedCapabilitiesOptions(options, language, enumType), - ]) + return getApp(homey).getAtaCapabilities() }, async getAtaValues({ homey, - params: { zoneId, zoneType }, + params, }: { homey: Homey params: ZoneData }): Promise { - return getFacade(homey, zoneType, Number(zoneId)).getAta() + return getApp(homey).getAtaValues(params) }, getBuildings(): BuildingZone[] { - return BuildingModel.getAll().sort(compareNames).map(mapBuilding) + return getBuildings() }, getDeviceSettings({ homey }: { homey: Homey }): DeviceSettings { - return (homey.app as MELCloudApp) - .getDevices() - .reduce((acc, device) => { - const driverId = device.driver.id - acc[driverId] ??= {} - Object.entries(device.getSettings() as Settings).forEach( - ([settingId, value]) => { - acc[driverId][settingId] ??= [] - if (!acc[driverId][settingId].includes(value)) { - acc[driverId][settingId].push(value) - } - }, - ) - return acc - }, {}) + return getApp(homey).getDeviceSettings() }, getDriverSettings({ homey, }: { homey: Homey }): Partial> { - const language = homey.i18n.getLanguage() - return Object.groupBy( - (homey.manifest as Manifest).drivers.flatMap((driver) => [ - ...getDriverSettings(driver, language), - ...getDriverLoginSetting(driver, language), - ]), - ({ driverId, groupId }) => groupId ?? driverId, - ) + return getApp(homey).getDriverSettings() }, async getErrors({ homey, @@ -258,25 +62,25 @@ export = { homey: Homey query: ErrorLogQuery }): Promise { - return (homey.app as MELCloudApp).facadeManager.getErrors(query) + return getApp(homey).getErrors(query) }, async getFrostProtectionSettings({ homey, - params: { zoneId, zoneType }, + params, }: { homey: Homey params: ZoneData }): Promise { - return getFacade(homey, zoneType, Number(zoneId)).getFrostProtection() + return getApp(homey).getFrostProtectionSettings(params) }, async getHolidayModeSettings({ homey, - params: { zoneId, zoneType }, + params, }: { homey: Homey params: ZoneData }): Promise { - return getFacade(homey, zoneType, Number(zoneId)).getHolidayMode() + return getApp(homey).getHolidayModeSettings(params) }, getLanguage({ homey }: { homey: Homey }): string { return homey.i18n.getLanguage() @@ -288,21 +92,18 @@ export = { body: LoginCredentials homey: Homey }): Promise { - return (homey.app as MELCloudApp).api.login(body) + return getApp(homey).login(body) }, async setAtaValues({ body, homey, - params: { zoneId, zoneType }, + params, }: { body: GroupAtaState homey: Homey params: ZoneData }): Promise { - handleResponse( - (await getFacade(homey, zoneType, Number(zoneId)).setAta(body)) - .AttributeErrors, - ) + return getApp(homey).setAtaValues(body, params) }, async setDeviceSettings({ body, @@ -313,59 +114,28 @@ export = { homey: Homey query?: { driverId: string } }): Promise { - await Promise.all( - (homey.app as MELCloudApp) - .getDevices({ driverId: query?.driverId }) - .map(async (device) => { - const changedKeys = Object.keys(body).filter( - (changedKey) => body[changedKey] !== device.getSetting(changedKey), - ) - if (changedKeys.length) { - await device.setSettings( - Object.fromEntries(changedKeys.map((key) => [key, body[key]])), - ) - await device.onSettings({ - changedKeys, - newSettings: device.getSettings() as Settings, - }) - } - }), - ) + return getApp(homey).setDeviceSettings(body, query?.driverId) }, async setFrostProtectionSettings({ body, homey, - params: { zoneId, zoneType }, + params, }: { body: FrostProtectionSettings homey: Homey params: ZoneData }): Promise { - handleResponse( - ( - await getFacade(homey, zoneType, Number(zoneId)).setFrostProtection( - body, - ) - ).AttributeErrors, - ) + return getApp(homey).setFrostProtectionSettings(body, params) }, async setHolidayModeSettings({ - body: { enabled, from, to }, + body, homey, - params: { zoneId, zoneType }, + params, }: { body: HolidayModeSettings homey: Homey params: ZoneData }): Promise { - handleResponse( - ( - await getFacade(homey, zoneType, Number(zoneId)).setHolidayMode({ - enabled, - from, - to, - }) - ).AttributeErrors, - ) + return getApp(homey).setHolidayModeSettings(body, params) }, } diff --git a/app.json b/app.json index 866c82bd..9ae10859 100644 --- a/app.json +++ b/app.json @@ -5191,6 +5191,40 @@ ] } ], + "widgets": { + "ata-group-setting": { + "api": { + "getAtaCapabilities": { + "method": "GET", + "path": "/capabilities/drivers/melcloud" + }, + "getAtaValues": { + "method": "GET", + "path": "/drivers/melcloud/:zoneType/:zoneId" + }, + "getBuildings": { + "method": "GET", + "path": "/buildings" + }, + "setAtaValues": { + "method": "PUT", + "path": "/drivers/melcloud/:zoneType/:zoneId" + } + }, + "height": 270, + "name": { + "da": "Luft-luft-enhedsindstilling", + "en": "Air-to-air device setting", + "es": "Ajuste de dispositivos aire-aire", + "fr": "Réglage des appareils air-air", + "nl": "Lucht-lucht apparaatinstelling", + "no": "Luft-luft-enhetsinnstilling", + "sv": "Luft-luft-enhetsinställning" + }, + "settings": [], + "id": "ata-group-setting" + } + }, "capabilities": { "fan_power": { "getable": true, diff --git a/app.ts b/app.ts index 39a2675a..cf4b61c5 100644 --- a/app.ts +++ b/app.ts @@ -1,16 +1,136 @@ -import MELCloudAPI, { FacadeManager } from '@olivierzal/melcloud-api' import 'core-js/actual/object/group-by' -import { App } from 'homey' -import { Settings as LuxonSettings } from 'luxon' import 'source-map-support/register' -import type { MELCloudDevice, Manifest } from './types' +import MELCloudAPI, { + FacadeManager, + FanSpeed, + Horizontal, + OperationMode, + Vertical, + type AreaFacade, + type BuildingFacade, + type DeviceFacadeAny, + type FloorFacade, + type FrostProtectionData, + type GroupAtaState, + type HolidayModeData, + type LoginCredentials, +} from '@olivierzal/melcloud-api' +import { App } from 'homey' +import fanSpeed from 'homey-lib/assets/capability/capabilities/fan_speed.json' +import power from 'homey-lib/assets/capability/capabilities/onoff.json' +import setTemperature from 'homey-lib/assets/capability/capabilities/target_temperature.json' +import thermostatMode from 'homey-lib/assets/capability/capabilities/thermostat_mode.json' +import { Settings as LuxonSettings } from 'luxon' import changelog from './.homeychangelog.json' +import horizontal from './.homeycompose/capabilities/horizontal.json' +import vertical from './.homeycompose/capabilities/vertical.json' +import { + fanSpeedValues, + modelClass, + type DeviceSettings, + type DriverCapabilitiesOptions, + type DriverSetting, + type ErrorLog, + type ErrorLogQuery, + type FrostProtectionSettings, + type HolidayModeSettings, + type LoginSetting, + type MELCloudDevice, + type Manifest, + type ManifestDriver, + type ManifestDriverCapabilitiesOptions, + type Settings, + type ZoneData, +} from './types' const NOTIFICATION_DELAY = 10000 +const formatErrors = (errors: Record): string => + Object.entries(errors) + .map(([error, messages]) => `${error}: ${messages.join(', ')}`) + .join('\n') + +const handleResponse = ( + errors: Record | null, +): void => { + if (errors) { + throw new Error(formatErrors(errors)) + } +} + +const getDriverSettings = ( + { id: driverId, settings }: ManifestDriver, + language: string, +): DriverSetting[] => + (settings ?? []).flatMap(({ children, id: groupId, label: groupLabel }) => + (children ?? []).map(({ id, label, max, min, type, units, values }) => ({ + driverId, + groupId, + groupLabel: groupLabel[language] ?? groupLabel.en, + id, + max, + min, + title: label[language] ?? label.en, + type, + units, + values: values?.map(({ id: valueId, label: valueLabel }) => ({ + id: valueId, + label: valueLabel[language] ?? valueLabel.en, + })), + })), + ) + +const getDriverLoginSetting = ( + { id: driverId, pair }: ManifestDriver, + language: string, +): DriverSetting[] => + Object.values( + Object.entries( + pair?.find( + (pairSetting): pairSetting is LoginSetting => + pairSetting.id === 'login', + )?.options ?? [], + ).reduce>((acc, [option, label]) => { + const isPassword = option.startsWith('password') + const key = isPassword ? 'password' : 'username' + acc[key] ??= { + driverId, + groupId: 'login', + id: key, + title: '', + type: isPassword ? 'password' : 'text', + } + acc[key][option.endsWith('Placeholder') ? 'placeholder' : 'title'] = + label[language] ?? label.en + return acc + }, {}), + ) + +const getLocalizedCapabilitiesOptions = ( + options: ManifestDriverCapabilitiesOptions, + language: string, + enumType?: + | typeof FanSpeed + | typeof Horizontal + | typeof OperationMode + | typeof Vertical, +): DriverCapabilitiesOptions => ({ + title: options.title[language] ?? options.title.en, + type: options.type, + values: options.values?.map(({ id, title }) => ({ + id: + enumType && id in enumType ? + String(enumType[id as keyof typeof enumType]) + : id, + label: title[language] ?? title.en, + })), +}) + export = class extends App { + readonly #language = this.homey.i18n.getLanguage() + #api!: MELCloudAPI #facadeManager!: FacadeManager @@ -19,17 +139,12 @@ export = class extends App { return this.#api } - public get facadeManager(): FacadeManager { - return this.#facadeManager - } - public override async onInit(): Promise { - const language = this.homey.i18n.getLanguage() const timezone = this.homey.clock.getTimezone() - LuxonSettings.defaultLocale = language LuxonSettings.defaultZone = timezone + LuxonSettings.defaultLocale = this.#language this.#api = await MELCloudAPI.create({ - language, + language: this.#language, logger: { error: (...args) => { this.error(...args) @@ -43,7 +158,7 @@ export = class extends App { timezone, }) this.#facadeManager = new FacadeManager(this.#api) - this.#createNotification(language) + this.#createNotification() } public override async onUninit(): Promise { @@ -51,18 +166,178 @@ export = class extends App { return Promise.resolve() } - public getDevices({ - driverId, - }: { driverId?: string } = {}): MELCloudDevice[] { - return ( - driverId === undefined ? - Object.values(this.homey.drivers.getDrivers()) - : [this.homey.drivers.getDriver(driverId)]).flatMap( - (driver) => driver.getDevices() as MELCloudDevice[], + public getAtaCapabilities(): [ + keyof GroupAtaState, + DriverCapabilitiesOptions, + ][] { + return [ + { key: 'Power', options: power }, + { key: 'SetTemperature', options: setTemperature }, + { + enumType: FanSpeed, + key: 'FanSpeed', + options: { ...fanSpeed, type: 'enum', values: fanSpeedValues }, + }, + { enumType: Vertical, key: 'VaneVerticalDirection', options: vertical }, + { + enumType: Horizontal, + key: 'VaneHorizontalDirection', + options: horizontal, + }, + { + enumType: OperationMode, + key: 'OperationMode', + options: { + ...thermostatMode, + values: (this.homey.manifest as Manifest).drivers + .find(({ id }) => id === 'melcloud') + ?.capabilitiesOptions?.thermostat_mode.values?.filter( + ({ id }) => id !== 'off', + ), + }, + }, + ].map(({ enumType, key, options }) => [ + key as keyof GroupAtaState, + getLocalizedCapabilitiesOptions(options, this.#language, enumType), + ]) + } + + public async getAtaValues({ + zoneId, + zoneType, + }: ZoneData): Promise { + return this.getFacade(zoneType, Number(zoneId)).getAta() + } + + public getDeviceSettings(): DeviceSettings { + return this.#getDevices().reduce((acc, device) => { + const driverId = device.driver.id + acc[driverId] ??= {} + Object.entries(device.getSettings() as Settings).forEach( + ([settingId, value]) => { + acc[driverId][settingId] ??= [] + if (!acc[driverId][settingId].includes(value)) { + acc[driverId][settingId].push(value) + } + }, + ) + return acc + }, {}) + } + + public getDriverSettings(): Partial> { + return Object.groupBy( + (this.homey.manifest as Manifest).drivers.flatMap((driver) => [ + ...getDriverSettings(driver, this.#language), + ...getDriverLoginSetting(driver, this.#language), + ]), + ({ driverId, groupId }) => groupId ?? driverId, ) } - #createNotification(language: string): void { + public async getErrors(query: ErrorLogQuery): Promise { + return this.#facadeManager.getErrors(query) + } + + public getFacade(zoneType: 'devices', id: number): DeviceFacadeAny + public getFacade( + zoneType: 'areas' | 'buildings' | 'floors', + id: number, + ): AreaFacade | BuildingFacade | FloorFacade + public getFacade( + zoneType: keyof typeof modelClass, + id: number, + ): AreaFacade | BuildingFacade | DeviceFacadeAny | FloorFacade { + const model = modelClass[zoneType].getById(id) + if (!model) { + throw new Error( + this.homey.__( + `errors.${zoneType === 'devices' ? 'device' : 'zone'}NotFound`, + ), + ) + } + return this.#facadeManager.get(model) + } + + public async getFrostProtectionSettings({ + zoneId, + zoneType, + }: ZoneData): Promise { + return this.getFacade(zoneType, Number(zoneId)).getFrostProtection() + } + + public async getHolidayModeSettings({ + zoneId, + zoneType, + }: ZoneData): Promise { + return this.getFacade(zoneType, Number(zoneId)).getHolidayMode() + } + + public getLanguage(): string { + return this.homey.i18n.getLanguage() + } + + public async login(credentials: LoginCredentials): Promise { + return this.api.login(credentials) + } + + public async setAtaValues( + state: GroupAtaState, + { zoneId, zoneType }: ZoneData, + ): Promise { + handleResponse( + (await this.getFacade(zoneType, Number(zoneId)).setAta(state)) + .AttributeErrors, + ) + } + + public async setDeviceSettings( + settings: Settings, + driverId?: string, + ): Promise { + await Promise.all( + this.#getDevices(driverId).map(async (device) => { + const changedKeys = Object.keys(settings).filter( + (changedKey) => + settings[changedKey] !== device.getSetting(changedKey), + ) + if (changedKeys.length) { + await device.setSettings( + Object.fromEntries(changedKeys.map((key) => [key, settings[key]])), + ) + await device.onSettings({ + changedKeys, + newSettings: device.getSettings() as Settings, + }) + } + }), + ) + } + + public async setFrostProtectionSettings( + settings: FrostProtectionSettings, + { zoneId, zoneType }: ZoneData, + ): Promise { + handleResponse( + ( + await this.getFacade(zoneType, Number(zoneId)).setFrostProtection( + settings, + ) + ).AttributeErrors, + ) + } + + public async setHolidayModeSettings( + settings: HolidayModeSettings, + { zoneId, zoneType }: ZoneData, + ): Promise { + handleResponse( + (await this.getFacade(zoneType, Number(zoneId)).setHolidayMode(settings)) + .AttributeErrors, + ) + } + + #createNotification(): void { const { version } = this.homey.manifest as Manifest if ( this.homey.settings.get('notifiedVersion') !== version && @@ -74,8 +349,8 @@ export = class extends App { await this.homey.notifications.createNotification({ excerpt: versionChangelog[ - language in versionChangelog ? - (language as keyof typeof versionChangelog) + this.#language in versionChangelog ? + (this.#language as keyof typeof versionChangelog) : 'en' ], }) @@ -85,9 +360,18 @@ export = class extends App { } } + #getDevices(driverId?: string): MELCloudDevice[] { + return ( + driverId === undefined ? + Object.values(this.homey.drivers.getDrivers()) + : [this.homey.drivers.getDriver(driverId)]).flatMap( + (driver) => driver.getDevices() as MELCloudDevice[], + ) + } + async #syncFromDevices(): Promise { await Promise.all( - this.getDevices().map(async (device) => device.syncFromDevice()), + this.#getDevices().map(async (device) => device.syncFromDevice()), ) } } diff --git a/bases/device.ts b/bases/device.ts index 3953806c..96dfd3fd 100644 --- a/bases/device.ts +++ b/bases/device.ts @@ -1,19 +1,10 @@ -import { - type DeviceFacade, - type DeviceType, - type EnergyData, - type ListDevice, - type UpdateDeviceData, - DeviceModel, -} from '@olivierzal/melcloud-api' import { Device } from 'homey' -import { type DurationLike, DateTime } from 'luxon' - -import type MELCloudApp from '../app' +import { DateTime, type DurationLike } from 'luxon' import addToLogs from '../decorators/addToLogs' import withTimers from '../mixins/withTimers' import { + K_MULTIPLIER, type Capabilities, type CapabilitiesOptions, type ConvertFromDevice, @@ -31,9 +22,18 @@ import { type SetCapabilities, type SetCapabilityTagMapping, type Settings, - K_MULTIPLIER, } from '../types' +import type { + DeviceFacade, + DeviceType, + EnergyData, + ListDevice, + UpdateDeviceData, +} from '@olivierzal/melcloud-api' + +import type MELCloudApp from '../app' + const INITIAL_SUM = 0 const MINIMUM_DIVISOR = 1 const SYNC_DELAY = 1000 @@ -350,15 +350,14 @@ export default abstract class< async #fetchDevice(): Promise { if (!this.#device) { - this.#device = (this.homey.app as MELCloudApp).facadeManager.get( - DeviceModel.getById((this.getData() as DeviceDetails['data']).id) as - | DeviceModel - | undefined, - ) - if (this.#device) { + try { + this.#device = (this.homey.app as MELCloudApp).getFacade( + 'devices', + (this.getData() as DeviceDetails['data']).id, + ) as DeviceFacade[T] await this.#init(this.#device.data) - } else { - await this.setWarning(this.homey.__('warnings.deviceNotFound')) + } catch (error) { + await this.setWarning(getErrorMessage(error)) } } return this.#device diff --git a/bases/driver.ts b/bases/driver.ts index 9981fa90..b4efa23c 100644 --- a/bases/driver.ts +++ b/bases/driver.ts @@ -1,14 +1,14 @@ -import type PairSession from 'homey/lib/PairSession' - import { + DeviceModel, type DeviceType, type EnergyData, type ListDevice, type LoginCredentials, - DeviceModel, } from '@olivierzal/melcloud-api' import { Driver } from 'homey' +import type PairSession from 'homey/lib/PairSession' + import type MELCloudApp from '..' import type { Capabilities, diff --git a/drivers/melcloud/device.ts b/drivers/melcloud/device.ts index 7472f2b4..d001c8d9 100644 --- a/drivers/melcloud/device.ts +++ b/drivers/melcloud/device.ts @@ -1,19 +1,19 @@ import { - type ListDeviceDataAta, FanSpeed, Horizontal, OperationMode, Vertical, + type ListDeviceDataAta, } from '@olivierzal/melcloud-api' import BaseMELCloudDevice from '../../bases/device' import { + ThermostatModeAta, type ConvertFromDevice, type ConvertToDevice, type OpCapabilitiesAta, type ReportPlanParameters, type SetCapabilitiesAta, - ThermostatModeAta, } from '../../types' export = class extends BaseMELCloudDevice<'Ata'> { diff --git a/drivers/melcloud_atw/device.ts b/drivers/melcloud_atw/device.ts index 6da3f433..a208e92a 100644 --- a/drivers/melcloud_atw/device.ts +++ b/drivers/melcloud_atw/device.ts @@ -1,23 +1,23 @@ import { - type ListDeviceDataAtw, - type ZoneAtw, OperationModeState, OperationModeZone, + type ListDeviceDataAtw, + type ZoneAtw, } from '@olivierzal/melcloud-api' import { DateTime } from 'luxon' import BaseMELCloudDevice from '../../bases/device' import { + HotWaterMode, + K_MULTIPLIER, + OperationModeStateHotWaterCapability, + OperationModeStateZoneCapability, type ConvertFromDevice, type ConvertToDevice, type OpCapabilitiesAtw, type ReportPlanParameters, type SetCapabilitiesAtw, type TargetTemperatureFlowCapabilities, - HotWaterMode, - K_MULTIPLIER, - OperationModeStateHotWaterCapability, - OperationModeStateZoneCapability, } from '../../types' const convertFromDeviceMeasurePower = ((value: number) => diff --git a/drivers/melcloud_atw/driver.ts b/drivers/melcloud_atw/driver.ts index bee154bb..17f6aad7 100644 --- a/drivers/melcloud_atw/driver.ts +++ b/drivers/melcloud_atw/driver.ts @@ -1,15 +1,15 @@ -import type { ListDeviceDataAtw } from '@olivierzal/melcloud-api' - import BaseMELCloudDriver from '../../bases/driver' import { - type CapabilitiesAtw, energyCapabilityTagMappingAtw, getCapabilitiesOptionsAtw, getCapabilityTagMappingAtw, listCapabilityTagMappingAtw, setCapabilityTagMappingAtw, + type CapabilitiesAtw, } from '../../types' +import type { ListDeviceDataAtw } from '@olivierzal/melcloud-api' + export = class extends BaseMELCloudDriver<'Atw'> { public readonly energyCapabilityTagMapping = energyCapabilityTagMappingAtw diff --git a/drivers/melcloud_erv/device.ts b/drivers/melcloud_erv/device.ts index 1860b5b1..8fa7f712 100644 --- a/drivers/melcloud_erv/device.ts +++ b/drivers/melcloud_erv/device.ts @@ -1,15 +1,15 @@ import { - type ListDeviceDataErv, VentilationMode, + type ListDeviceDataErv, } from '@olivierzal/melcloud-api' import BaseMELCloudDevice from '../../bases/device' import { + ThermostatModeErv, type ConvertFromDevice, type ConvertToDevice, type OpCapabilitiesErv, type SetCapabilitiesErv, - ThermostatModeErv, } from '../../types' export = class extends BaseMELCloudDevice<'Erv'> { diff --git a/drivers/melcloud_erv/driver.ts b/drivers/melcloud_erv/driver.ts index 95ff73f4..5ea4c33f 100644 --- a/drivers/melcloud_erv/driver.ts +++ b/drivers/melcloud_erv/driver.ts @@ -1,5 +1,3 @@ -import type { ListDeviceDataErv } from '@olivierzal/melcloud-api' - import BaseMELCloudDriver from '../../bases/driver' import { energyCapabilityTagMappingErv, @@ -9,6 +7,8 @@ import { setCapabilityTagMappingErv, } from '../../types' +import type { ListDeviceDataErv } from '@olivierzal/melcloud-api' + export = class extends BaseMELCloudDriver<'Erv'> { public readonly energyCapabilityTagMapping = energyCapabilityTagMappingErv diff --git a/eslint.config.mjs b/eslint.config.mjs index b5267903..a7791d72 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -122,8 +122,32 @@ const classGroups = { groups, } +const importGroups = { + groups: [ + 'side-effect', + 'side-effect-style', + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'object', + 'style', + 'unknown', + 'builtin-type', + 'external-type', + 'internal-type', + 'parent-type', + 'sibling-type', + 'index-type', + 'type', + ], +} + const typeGroups = { groups: [ + 'import', 'keyword', 'literal', 'named', @@ -132,9 +156,8 @@ const typeGroups = { 'tuple', 'union', 'intersection', - 'operator', 'conditional', - 'import', + 'operator', 'unknown', 'nullish', ], @@ -144,8 +167,8 @@ const requiredFirst = { groupKind: 'required-first', } -const typesFirst = { - groupKind: 'types-first', +const valuesFirst = { + groupKind: 'values-first', } export default [ @@ -293,13 +316,13 @@ export default [ 'perfectionist/sort-array-includes': 'error', 'perfectionist/sort-classes': ['error', classGroups], 'perfectionist/sort-enums': 'error', - 'perfectionist/sort-exports': ['error', typesFirst], - 'perfectionist/sort-imports': 'error', + 'perfectionist/sort-exports': ['error', valuesFirst], + 'perfectionist/sort-imports': ['error', importGroups], 'perfectionist/sort-interfaces': ['error', requiredFirst], 'perfectionist/sort-intersection-types': ['error', typeGroups], 'perfectionist/sort-maps': 'error', - 'perfectionist/sort-named-exports': ['error', typesFirst], - 'perfectionist/sort-named-imports': ['error', typesFirst], + 'perfectionist/sort-named-exports': ['error', valuesFirst], + 'perfectionist/sort-named-imports': ['error', valuesFirst], 'perfectionist/sort-object-types': ['error', requiredFirst], 'perfectionist/sort-objects': 'error', 'perfectionist/sort-sets': 'error', diff --git a/lib/getBuildings.ts b/lib/getBuildings.ts new file mode 100644 index 00000000..d5f31868 --- /dev/null +++ b/lib/getBuildings.ts @@ -0,0 +1,41 @@ +import { + BuildingModel, + type AreaModelAny, + type FloorModel, +} from '@olivierzal/melcloud-api' + +import type { AreaZone, BuildingZone, FloorZone } from '../types' + +const compareNames = ( + { name: name1 }: { name: string }, + { name: name2 }: { name: string }, +): number => name1.localeCompare(name2) + +const mapArea = ({ id, name }: AreaModelAny): AreaZone => ({ + id, + name, +}) + +const mapFloor = ({ areas, id, name }: FloorModel): FloorZone => ({ + areas: areas.sort(compareNames).map(mapArea), + id, + name, +}) + +const mapBuilding = ({ + areas, + floors, + id, + name, +}: BuildingModel): BuildingZone => ({ + areas: areas + .filter(({ floorId }: { floorId: number | null }) => floorId === null) + .sort(compareNames) + .map(mapArea), + floors: floors.sort(compareNames).map(mapFloor), + id, + name, +}) + +export default (): BuildingZone[] => + BuildingModel.getAll().sort(compareNames).map(mapBuilding) diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 00000000..99b479bb --- /dev/null +++ b/lib/index.ts @@ -0,0 +1 @@ +export { default as getBuildings } from './getBuildings' diff --git a/locales/da.json b/locales/da.json index 47f4537d..c5439ee0 100644 --- a/locales/da.json +++ b/locales/da.json @@ -1,6 +1,6 @@ { "errors": { - "endDateMissing": "Slutdato mangler.", + "deviceNotFound": "Enhed ikke fundet.", "zoneNotFound": "Zone ikke fundet." }, "settings": { @@ -60,8 +60,8 @@ "holidayMode": { "enabled": "Aktiver ferietilstand", "endDate": "Slutdato", - "startDate": "Startdato", - "endDateMissing": "Slutdato mangler." + "endDateMissing": "Slutdato mangler.", + "startDate": "Startdato" }, "intError": "__name__: skal være et heltal mellem __min__ og __max__.", "refresh": "Genindlæs", @@ -69,7 +69,6 @@ "title": "MELCloud-indstillinger" }, "warnings": { - "dashboard": "Forlad enheden og vend tilbage for at opdatere dit instrumentbræt.", - "deviceNotFound": "Enhed ikke fundet." + "dashboard": "Forlad enheden og vend tilbage for at opdatere dit instrumentbræt." } } \ No newline at end of file diff --git a/locales/en.json b/locales/en.json index 699ef165..2a349f5f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,6 +1,6 @@ { "errors": { - "endDateMissing": "End date is missing.", + "deviceNotFound": "Device not found.", "zoneNotFound": "Zone not found." }, "settings": { @@ -60,8 +60,8 @@ "holidayMode": { "enabled": "Enable holiday mode", "endDate": "End date", - "startDate": "Start date", - "endDateMissing": "End date is missing." + "endDateMissing": "End date is missing.", + "startDate": "Start date" }, "intError": "__name__: must be an integer between __min__ and __max__.", "refresh": "Refresh", @@ -69,7 +69,6 @@ "title": "MELCloud settings" }, "warnings": { - "dashboard": "Exit the device and return to update your dashboard.", - "deviceNotFound": "Device not found." + "dashboard": "Exit the device and return to update your dashboard." } } \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index f790969a..1edb8cf1 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,6 +1,6 @@ { "errors": { - "endDateMissing": "Falta la fecha de finalización.", + "deviceNotFound": "Dispositivo no encontrada.", "zoneNotFound": "Zona no encontrada." }, "settings": { @@ -60,8 +60,8 @@ "holidayMode": { "enabled": "Activar modo vacaciones", "endDate": "Fecha de finalización", - "startDate": "Fecha de inicio", - "endDateMissing": "Falta la fecha de finalización." + "endDateMissing": "Falta la fecha de finalización.", + "startDate": "Fecha de inicio" }, "intError": "__name__: debe ser un número entero entre __min__ y __max__.", "refresh": "Recargar", @@ -69,7 +69,6 @@ "title": "Configuración de MELCloud" }, "warnings": { - "dashboard": "Salga del dispositivo y regrese para actualizar su panel de control.", - "deviceNotFound": "Dispositivo no encontrada." + "dashboard": "Salga del dispositivo y regrese para actualizar su panel de control." } } \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 9099a75d..16336e93 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,6 +1,7 @@ { "errors": { - "zoneNotFound": "La zone est introuvable." + "deviceNotFound": "Appareil introuvable.", + "zoneNotFound": "Zone introuvable." }, "settings": { "authenticate": { @@ -68,7 +69,6 @@ "title": "Paramètres MELCloud" }, "warnings": { - "dashboard": "Sortez de l'appareil et revenez pour mettre à jour votre tableau de bord.", - "deviceNotFound": "L'appareil est introuvable." + "dashboard": "Sortez de l'appareil et revenez pour mettre à jour votre tableau de bord." } } \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json index 146f1c79..055ae197 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -1,6 +1,6 @@ { "errors": { - "endDateMissing": "Einddatum ontbreekt.", + "deviceNotFound": "Apparaat niet gevonden.", "zoneNotFound": "Zone niet gevonden." }, "settings": { @@ -60,8 +60,8 @@ "holidayMode": { "enabled": "Vakantiemodus inschakelen", "endDate": "Einddatum", - "startDate": "Begindatum", - "endDateMissing": "Einddatum ontbreekt." + "endDateMissing": "Einddatum ontbreekt.", + "startDate": "Begindatum" }, "intError": "__name__: moet een heel getal zijn tussen __min__ en __max__.", "refresh": "Herladen", @@ -69,7 +69,6 @@ "title": "MELCloud-instellingen" }, "warnings": { - "dashboard": "Verlaat het apparaat en keer terug om uw dashboard te updaten.", - "deviceNotFound": "Apparaat niet gevonden." + "dashboard": "Verlaat het apparaat en keer terug om uw dashboard te updaten." } } \ No newline at end of file diff --git a/locales/no.json b/locales/no.json index 8647d73c..eee9938b 100644 --- a/locales/no.json +++ b/locales/no.json @@ -1,6 +1,6 @@ { "errors": { - "endDateMissing": "Sluttdato mangler.", + "deviceNotFound": "Enhet ikke funnet.", "zoneNotFound": "Sone ikke funnet." }, "settings": { @@ -60,8 +60,8 @@ "holidayMode": { "enabled": "Aktiver feriemodus", "endDate": "Sluttdato", - "startDate": "Startdato", - "endDateMissing": "Sluttdato mangler." + "endDateMissing": "Sluttdato mangler.", + "startDate": "Startdato" }, "intError": "__name__: må være et heltall mellom __min__ og __max__.", "refresh": "Gjenta", @@ -69,7 +69,6 @@ "title": "MELCloud-innstillinger" }, "warnings": { - "dashboard": "Avslutt enheten og gå tilbake for å oppdatere dashbordet ditt.", - "deviceNotFound": "Enhet ikke funnet." + "dashboard": "Avslutt enheten og gå tilbake for å oppdatere dashbordet ditt." } } \ No newline at end of file diff --git a/locales/sv.json b/locales/sv.json index e39d0512..07d9cdf6 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -1,6 +1,6 @@ { "errors": { - "endDateMissing": "Slutdatum saknas.", + "deviceNotFound": "Enhet ej hittad.", "zoneNotFound": "Zon ej hittad." }, "settings": { @@ -60,8 +60,8 @@ "holidayMode": { "enabled": "Aktivera semesterläge", "endDate": "Slutdatum", - "startDate": "Startdatum", - "endDateMissing": "Slutdatum saknas." + "endDateMissing": "Slutdatum saknas.", + "startDate": "Startdatum" }, "intError": "__name__: måste vara ett heltal mellan __min__ och __max__.", "refresh": "Ladda om", @@ -69,7 +69,6 @@ "title": "MELCloud-inställningar" }, "warnings": { - "dashboard": "Avsluta enheten och återvänd för att uppdatera din instrumentpanel.", - "deviceNotFound": "Enhet ej hittad." + "dashboard": "Avsluta enheten och återvänd för att uppdatera din instrumentpanel." } } \ No newline at end of file diff --git a/mixins/withTimers.ts b/mixins/withTimers.ts index 4869e1eb..0ab535a5 100644 --- a/mixins/withTimers.ts +++ b/mixins/withTimers.ts @@ -1,8 +1,8 @@ +import { DateTime, Duration, type DurationLike } from 'luxon' + import type { SimpleClass } from 'homey' import type Homey from 'homey/lib/Homey' -import { type DurationLike, DateTime, Duration } from 'luxon' - type HomeyClass = new ( ...args: any[] ) => SimpleClass & { readonly homey: Homey } diff --git a/package-lock.json b/package-lock.json index 908715c3..8eceb0a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@stylistic/eslint-plugin": "^2.8.0", "@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.7", "@types/luxon": "^3.4.2", - "@types/node": "^22.7.3", + "@types/node": "^22.7.4", "eslint": "^9.11.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsonc": "^2.16.0", @@ -368,9 +368,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.3.tgz", - "integrity": "sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==", + "version": "22.7.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", + "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", "dev": true, "dependencies": { "undici-types": "~6.19.2" diff --git a/package.json b/package.json index 462a911f..8fad0ee0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@stylistic/eslint-plugin": "^2.8.0", "@types/homey": "npm:homey-apps-sdk-v3-types@^0.3.7", "@types/luxon": "^3.4.2", - "@types/node": "^22.7.3", + "@types/node": "^22.7.4", "eslint": "^9.11.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-jsonc": "^2.16.0", diff --git a/settings/index.html b/settings/index.html index 6705e994..b68eb4b6 100644 --- a/settings/index.html +++ b/settings/index.html @@ -292,4 +292,4 @@

- + \ No newline at end of file diff --git a/settings/index.ts b/settings/index.ts index 62222598..6d504669 100644 --- a/settings/index.ts +++ b/settings/index.ts @@ -810,13 +810,13 @@ const fetchAtaValues = async ( 'GET', `/drivers/melcloud/${zone.replace('_', '/')}`, async (error: Error | null, data: GroupAtaState) => { - unhide(hasZoneAtaDevicesElement, error === null) if (!error) { updateZoneMapping({ ...defaultAtaValues, ...data }, zone) refreshAtaValuesElement() } else if (error.message !== 'No air-to-air device found') { await homey.alert(error.message) } + unhide(hasZoneAtaDevicesElement, error === null) resolve() }, ) @@ -1432,11 +1432,6 @@ const addEventListeners = (homey: Homey): void => { } const load = async (homey: Homey): Promise => { - addEventListeners(homey) - generateCommonSettings(homey) - Object.keys(deviceSettings).forEach((driverId) => { - generateDriverSettings(homey, driverId) - }) if (homeySettings.contextKey !== undefined) { try { await fetchBuildings(homey) @@ -1453,6 +1448,11 @@ async function onHomeyReady(homey: Homey): Promise { await fetchAtaCapabilities(homey) await fetchDeviceSettings(homey) await fetchDriverSettings(homey) + generateCommonSettings(homey) + Object.keys(deviceSettings).forEach((driverId) => { + generateDriverSettings(homey, driverId) + }) + addEventListeners(homey) await load(homey) await homey.ready() } diff --git a/types/ata.ts b/types/ata.ts index a9e68871..4780c6d5 100644 --- a/types/ata.ts +++ b/types/ata.ts @@ -9,6 +9,7 @@ import type { } from '@olivierzal/melcloud-api' import type AtaDevice from '../drivers/melcloud/device' + import type { BaseGetCapabilities, BaseListCapabilities, diff --git a/types/atw.ts b/types/atw.ts index 3ca6f7b8..2951edfb 100644 --- a/types/atw.ts +++ b/types/atw.ts @@ -1,3 +1,5 @@ +import { title as thermostatModeTitle } from 'homey-lib/assets/capability/capabilities/thermostat_mode.json' + import type { EnergyDataAtw, GetDeviceDataAtw, @@ -8,9 +10,8 @@ import type { UpdateDeviceDataAtw, } from '@olivierzal/melcloud-api' -import { title as thermostatModeTitle } from 'homey-lib/assets/capability/capabilities/thermostat_mode.json' - import type AtwDevice from '../drivers/melcloud/device' + import type { BaseGetCapabilities, BaseListCapabilities, diff --git a/types/common.ts b/types/common.ts index aa957f71..85e3444c 100644 --- a/types/common.ts +++ b/types/common.ts @@ -1,6 +1,8 @@ -import type { DateObjectUnits, DurationLike } from 'luxon' - import { + AreaModel, + BuildingModel, + DeviceModel, + FloorModel, type BaseModel, type DeviceType, type EnergyData, @@ -11,17 +13,17 @@ import { type LoginCredentials, type SetDeviceData, type UpdateDeviceData, - AreaModel, - BuildingModel, - FloorModel, } from '@olivierzal/melcloud-api' +import type { DateObjectUnits, DurationLike } from 'luxon' + import type AtaDevice from '../drivers/melcloud/device' import type AtaDriver from '../drivers/melcloud/driver' import type AtwDevice from '../drivers/melcloud_atw/device' import type AtwDriver from '../drivers/melcloud_atw/driver' import type ErvDevice from '../drivers/melcloud_erv/device' import type ErvDriver from '../drivers/melcloud_erv/driver' + import type { CapabilitiesAta, EnergyCapabilitiesAta, @@ -329,11 +331,12 @@ export type Zone = AreaZone | BuildingZone | FloorZone export const modelClass = { areas: AreaModel, buildings: BuildingModel, + devices: DeviceModel, floors: FloorModel, } as const export interface ZoneData { zoneId: string - zoneType: keyof typeof modelClass + zoneType: Exclude } const addPrefixToTitle = ( diff --git a/types/erv.ts b/types/erv.ts index 45db3d00..88010c66 100644 --- a/types/erv.ts +++ b/types/erv.ts @@ -6,6 +6,7 @@ import type { } from '@olivierzal/melcloud-api' import type ErvDevice from '../drivers/melcloud/device' + import type { BaseGetCapabilities, BaseListCapabilities, diff --git a/types/index.ts b/types/index.ts index c2b2fcfe..6c7cb329 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,24 +1,23 @@ -export type { LocalizedStrings } from './bases' export { - type CapabilitiesAta, - type EnergyCapabilitiesAta, - type FlowArgsAta, - type OpCapabilitiesAta, - type SetCapabilitiesAta, ThermostatModeAta, energyCapabilityTagMappingAta, getCapabilityTagMappingAta, listCapabilityTagMappingAta, setCapabilityTagMappingAta, + type CapabilitiesAta, + type EnergyCapabilitiesAta, + type FlowArgsAta, + type OpCapabilitiesAta, + type SetCapabilitiesAta, } from './ata' export { + getCapabilitiesOptionsAtw, type CapabilitiesAtw, type EnergyCapabilitiesAtw, type FlowArgsAtw, type OpCapabilitiesAtw, type SetCapabilitiesAtw, type TargetTemperatureFlowCapabilities, - getCapabilitiesOptionsAtw, } from './atw' export { HotWaterMode, @@ -30,6 +29,10 @@ export { setCapabilityTagMappingAtw, } from './atw' export { + K_MULTIPLIER, + fanSpeedValues, + getCapabilitiesOptionsAtaErv, + modelClass, type AreaZone, type BuildingZone, type Capabilities, @@ -72,20 +75,17 @@ export { type ValueOf, type Zone, type ZoneData, - K_MULTIPLIER, - fanSpeedValues, - getCapabilitiesOptionsAtaErv, - modelClass, } from './common' export { - type CapabilitiesErv, - type EnergyCapabilitiesErv, - type FlowArgsErv, - type OpCapabilitiesErv, - type SetCapabilitiesErv, ThermostatModeErv, energyCapabilityTagMappingErv, getCapabilityTagMappingErv, listCapabilityTagMappingErv, setCapabilityTagMappingErv, + type CapabilitiesErv, + type EnergyCapabilitiesErv, + type FlowArgsErv, + type OpCapabilitiesErv, + type SetCapabilitiesErv, } from './erv' +export type { LocalizedStrings } from './bases' diff --git a/widgets/ata-group-setting/api.ts b/widgets/ata-group-setting/api.ts new file mode 100644 index 00000000..2b135221 --- /dev/null +++ b/widgets/ata-group-setting/api.ts @@ -0,0 +1,46 @@ +import { getBuildings } from '../../lib' + +import type { GroupAtaState } from '@olivierzal/melcloud-api' +import type Homey from 'homey/lib/Homey' + +import type MELCloudApp from '../..' +import type { + BuildingZone, + DriverCapabilitiesOptions, + ZoneData, +} from '../../types' + +const getApp = (homey: Homey): MELCloudApp => homey.app as MELCloudApp + +export = { + getAtaCapabilities({ + homey, + }: { + homey: Homey + }): [keyof GroupAtaState, DriverCapabilitiesOptions][] { + return getApp(homey).getAtaCapabilities() + }, + async getAtaValues({ + homey, + params, + }: { + homey: Homey + params: ZoneData + }): Promise { + return getApp(homey).getAtaValues(params) + }, + getBuildings(): BuildingZone[] { + return getBuildings() + }, + async setAtaValues({ + body, + homey, + params, + }: { + body: GroupAtaState + homey: Homey + params: ZoneData + }): Promise { + return getApp(homey).setAtaValues(body, params) + }, +} diff --git a/widgets/ata-group-setting/preview-dark.png b/widgets/ata-group-setting/preview-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..888755d4053c4922f42337d8adce8129eed33aed GIT binary patch literal 161716 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