UNPKG

@homebridge-plugins/homebridge-air

Version:

The AirNow plugin allows you to monitor the current AirQuality for your Zip Code from HomeKit and Siri.

167 lines 7.98 kB
import { devices } from 'homebridge'; import { AirQualitySensorMatter } from './devices/airqualitysensormatter.js'; import { AirPlatform } from './platform.js'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'; /** * Map a HomeKit AQI level (1-5) to a Matter AirQuality enum value. * * HomeKit levels: 1=Excellent, 2=Good, 3=Fair, 4=Inferior, 5=Poor * Matter AirQuality: 0=Unknown, 1=Good, 2=Fair, 3=Moderate, 4=Poor, 5=VeryPoor, 6=ExtremelyPoor */ function toMatterAirQuality(homeKitAQI) { switch (homeKitAQI) { case 1: return 1; // Excellent → Good case 2: return 2; // Good → Fair case 3: return 3; // Fair → Moderate case 4: return 5; // Inferior → VeryPoor case 5: return 6; // Poor → ExtremelyPoor default: return 0; // Unknown } } /** * AirMatterPlatform * * Extends AirPlatform (HAP) and overrides device discovery to register * Air Quality sensors as Matter accessories instead of HAP accessories. * * This platform is selected when `options.enableMatter` or `options.preferMatter` * is set to `true` in the plugin configuration AND Matter is available/enabled * in Homebridge. */ export class AirMatterPlatform extends AirPlatform { // Track cached Matter accessories (keyed by UUID) matterAccessories = new Map(); constructor(log, config, api) { super(log, config, api); if (!this.api.isMatterAvailable?.()) { this.log.warn('Matter is not available in this version of Homebridge. Please update Homebridge to use Matter features.'); } if (!this.api.isMatterEnabled?.()) { this.log.warn('Matter is not enabled in Homebridge. Please enable Matter in the Homebridge settings to use Matter features.'); } } /** * Override configureAccessory to unregister any previously cached HAP accessories. * * When a user switches from HAP to Matter mode, Homebridge will restore cached * HAP accessories on startup. In Matter mode these stale HAP accessories must be * removed so they do not accumulate untracked in the Homebridge accessory cache. */ async configureAccessory(accessory) { await this.debugLog(`Unregistering stale HAP accessory (Matter mode active): ${accessory.displayName}`); await Promise.resolve(this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])); } /** * Called when Homebridge restores cached Matter accessories from disk at startup. */ configureMatterAccessory(accessory) { this.log.debug(`Loading cached Matter accessory: ${accessory.displayName}`); this.matterAccessories.set(accessory.UUID, accessory); } /** * Override discoverDevices to register Air Quality devices as Matter accessories. */ async discoverDevices() { try { if (this.config.devices) { for (const device of this.config.devices) { device.city = device.city ? device.city : 'Unknown'; device.zipCode = device.zipCode ? device.zipCode : '00000'; device.provider = device.provider ? device.provider : 'Unknown'; if (device.latitude && device.longitude) { try { device.latitude = Number.parseFloat(Number.parseFloat(device.latitude.toString()).toFixed(6)); device.longitude = Number.parseFloat(Number.parseFloat(device.longitude.toString()).toFixed(6)); } catch { await this.errorLog('Latitude and Longitude must be a number'); } } await this.debugLog(`Discovered ${device.city}`); await this.createMatterAirQualitySensor(device); } } } catch { await this.errorLog('discoverDevices, No Device Config'); } } /** * Register or restore a single Air Quality device as a Matter accessory and * start its polling loop. */ async createMatterAirQualitySensor(device) { const uuidString = (device.latitude && device.longitude) ? (`${device.latitude}` + `${device.longitude}` + `${device.provider}`) : (`${device.zipCode}` + `${device.city}` + `${device.provider}`); const uuid = this.api.hap.uuid.generate(uuidString); // Handle hide_device: remove any existing Matter accessory then bail out. if (device.hide_device) { const existingAccessory = this.matterAccessories.get(uuid); if (existingAccessory) { await this.warnLog(`Removing Matter accessory for hidden device: ${existingAccessory.displayName}`); await this.api.matter.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); this.matterAccessories.delete(uuid); } else { await this.debugLog(`Skipping hidden device (no cached Matter accessory): ${device.city}`); } return; } const displayName = await this.validateAndCleanDisplayName(device.city ?? 'Unknown', 'city', device.city ?? 'Unknown', device.provider); const manufacturer = device.provider === 'airnow' ? 'AirNow' : device.provider === 'aqicn' ? 'Aqicn' : 'Unknown'; const firmwareRevision = device.firmware ?? await this.getVersion(); const existingAccessory = this.matterAccessories.get(uuid); if (existingAccessory) { await this.infoLog(`Restoring existing Matter accessory from cache: ${displayName}`); // Update context with latest device info if (existingAccessory.context) { existingAccessory.context.device = device; existingAccessory.context.serialNumber = device.zipCode ?? '00000'; existingAccessory.context.model = manufacturer; existingAccessory.context.FirmwareRevision = firmwareRevision; } await this.api.matter.updatePlatformAccessories([existingAccessory]); } else { await this.infoLog(`Adding new Matter accessory: ${displayName}`); const accessory = { UUID: uuid, displayName, deviceType: devices.AirQualitySensorDevice, serialNumber: device.zipCode ?? '00000', manufacturer, model: manufacturer, firmwareRevision, context: { device, serialNumber: device.zipCode ?? '00000', model: manufacturer, FirmwareRevision: firmwareRevision, }, clusters: { airQuality: { airQuality: toMatterAirQuality(0), }, }, }; this.matterAccessories.set(uuid, accessory); await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); await this.debugLog(`${device.city} uuid: ${uuidString}`); } // Start polling loop: fetch AQI from the provider API and push to Matter state. new AirQualitySensorMatter(this, device, uuid); } /** * Update the air quality state for a device after fetching fresh data. * * @param uuid - The Matter accessory UUID. * @param homeKitAQI - HomeKit AQI level (1-5). */ async updateMatterAirQuality(uuid, homeKitAQI) { const matterAQ = toMatterAirQuality(homeKitAQI); await this.api.matter.updateAccessoryState(uuid, 'airQuality', { airQuality: matterAQ }); await this.debugLog(`Updated Matter air quality for ${uuid}: ${matterAQ}`); } } //# sourceMappingURL=AirMatterPlatform.js.map