Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions README.md

Large diffs are not rendered by default.

108 changes: 101 additions & 7 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,43 @@ export interface DeviceFeature {
unit?: string;
min?: number;
max?: number;
/**
* Resolution the physical device accepts for a setpoint (finite number > 0,
* e.g. 0.5 for an AC steppable by half a degree), honored by the dashboard
* +/- buttons. Absent or null = nothing declared, the UI keeps its
* per-category default.
*/
step?: number | null;
read_only?: boolean;
has_feedback?: boolean;
keep_history?: boolean;
last_value?: number;
last_value_string?: string;
/**
* For an enum-like feature, the subset of values THIS device actually
* supports (camera movements and presets, AC modes…): the taxonomy defines
* the full generic value set, the integration narrows it per device. String
* values are only accepted on the `text`/`select` feature type (dynamic
* selects: installed TV apps, HDMI sources…). On re-publish of an
* already-created device, the options are silently upserted by the
* supervisor like the `params` (matched by feature external_id).
*/
supported_options?: DeviceFeatureSupportedOption[];
[key: string]: unknown;
}

/**
* One labeled option of an enum-like feature (`supported_options`): `value` is
* the scalar sent as the command value (integers everywhere; strings only on
* the `text`/`select` type), `label` the human name, `sort_order` the display
* order (defaulted to the array index).
*/
export interface DeviceFeatureSupportedOption {
value: number | string;
label: string;
sort_order?: number;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/** A device param (free key/value attached to a device). */
export interface DeviceParam {
name: string;
Expand Down Expand Up @@ -187,6 +216,20 @@ export interface NetworkActiveScanOptions extends NetworkScanOptions {
payload: Buffer | string;
}

/** Options of a Wake-on-LAN emission (contract C.3). */
export interface WakeOnLanOptions {
/**
* Destination IPv4 address. Default: 255.255.255.255 (the limited
* broadcast — use the subnet broadcast, e.g. `192.168.1.255`, when the
* device ignores the limited one).
*/
address?: string;
/** Destination UDP port. Default: 9. */
port?: number;
/** Source UDP port. Default: 0 (ephemeral port chosen by the OS). */
sourcePort?: number;
}

/** Raw result of a 'udp-broadcast' mediated scan: one received datagram. */
export interface UdpBroadcastScanResult {
source_ip: string;
Expand Down Expand Up @@ -605,15 +648,18 @@ export declare const DEVICE_FEATURE_CATEGORIES: {
readonly ENERGY_SENSOR: 'energy-sensor';
readonly ENERGY_PRODUCTION_SENSOR: 'energy-production-sensor';
readonly FAN: 'fan';
readonly GRID_SENSOR: 'grid-sensor';
readonly HEATER: 'heater';
readonly HEPA_FILTER_MONITORING: 'hepa-filter-monitoring';
readonly HOME_OUTPUT_SENSOR: 'home-output-sensor';
readonly HUMIDITY_SENSOR: 'humidity-sensor';
readonly LEAK_SENSOR: 'leak-sensor';
readonly LIGHT: 'light';
readonly LIGHT_SENSOR: 'light-sensor';
readonly LEVEL_SENSOR: 'level-sensor';
readonly MOTION_SENSOR: 'motion-sensor';
readonly LOCK: 'lock';
readonly MAINTENANCE: 'maintenance';
readonly MUSIC: 'music';
readonly NOISE_SENSOR: 'noise-sensor';
readonly OPENING_SENSOR: 'opening-sensor';
Expand All @@ -622,6 +668,9 @@ export declare const DEVICE_FEATURE_CATEGORIES: {
readonly PM25_SENSOR: 'pm25-sensor';
readonly PM10_SENSOR: 'pm10-sensor';
readonly FORMALDEHYD_SENSOR: 'formaldehyd-sensor';
readonly NO2_SENSOR: 'no2-sensor';
readonly O3_SENSOR: 'o3-sensor';
readonly SO2_SENSOR: 'so2-sensor';
readonly PRECIPITATION_SENSOR: 'precipitation-sensor';
readonly PRESENCE_SENSOR: 'presence-sensor';
readonly PRESSURE_SENSOR: 'pressure-sensor';
Expand Down Expand Up @@ -698,6 +747,11 @@ export declare const DEVICE_FEATURE_TYPES: {
};
readonly CAMERA: {
readonly IMAGE: 'image';
readonly MOVE: 'move';
readonly PRESET: 'preset';
readonly PAN_POSITION: 'pan-position';
readonly TILT_POSITION: 'tilt-position';
readonly ZOOM_POSITION: 'zoom-position';
};
readonly CHARGING_STATION: {
readonly CONNECTOR_STATUS: 'connector-status';
Expand All @@ -710,6 +764,7 @@ export declare const DEVICE_FEATURE_TYPES: {
readonly BINARY: 'binary';
readonly LMH_VOLUME: 'lmh_volume';
readonly MELODY: 'melody';
readonly TEST_IN_PROGRESS: 'test-in-progress';
};
readonly CHILD_LOCK: {
readonly BINARY: 'binary';
Expand Down Expand Up @@ -823,12 +878,26 @@ export declare const DEVICE_FEATURE_TYPES: {
readonly THIRTY_MINUTES_CONSUMPTION_COST: 'thirty-minutes-consumption-cost';
};
readonly ENERGY_PRODUCTION_SENSOR: {
readonly POWER: 'power';
readonly INDEX: 'index';
readonly DAILY_PRODUCTION: 'daily-production';
readonly DAILY_PRODUCTION_REVENUE: 'daily-production-revenue';
readonly THIRTY_MINUTES_PRODUCTION: 'thirty-minutes-production';
readonly THIRTY_MINUTES_PRODUCTION_REVENUE: 'thirty-minutes-production-revenue';
};
readonly GRID_SENSOR: {
readonly INPUT_POWER: 'input-power';
readonly OUTPUT_POWER: 'output-power';
readonly POWER: 'power';
readonly INPUT_INDEX: 'input-index';
readonly OUTPUT_INDEX: 'output-index';
};
readonly HOME_OUTPUT_SENSOR: {
readonly POWER: 'power';
readonly INDEX: 'index';
readonly OFF_GRID_POWER: 'off-grid-power';
readonly OFF_GRID_INDEX: 'off-grid-index';
};
readonly BATTERY_STORAGE: {
readonly BATTERY_LEVEL: 'battery-level';
readonly CHARGE_POWER: 'charge-power';
Expand Down Expand Up @@ -958,6 +1027,7 @@ export declare const DEVICE_FEATURE_TYPES: {
};
readonly TEXT: {
readonly TEXT: 'text';
readonly SELECT: 'select';
};
readonly RISK: {
readonly INTEGER: 'integer';
Expand Down Expand Up @@ -1034,6 +1104,9 @@ export declare const DEVICE_FEATURE_TYPES: {
readonly FILTER_MONITORING: {
readonly FILTER_LIFE_REMAINING: 'filter-life-remaining';
};
readonly MAINTENANCE: {
readonly LIFE_REMAINING: 'life-remaining';
};
readonly VACUUM_CLEANER: {
readonly STATE: 'state';
readonly RUN_MODE: 'run-mode';
Expand Down Expand Up @@ -1400,8 +1473,25 @@ export declare class GladysIntegration extends EventEmitter {
scanNetwork(type: 'ssdp', options?: NetworkScanOptions): Promise<SsdpScanResult[]>;
scanNetwork(type: string, options?: NetworkScanOptions & Partial<NetworkActiveScanOptions>): Promise<unknown[]>;

/** Handler called when the user actions a device feature (auto-acked). */
onSetValue(callback: (device: Device, deviceFeature: DeviceFeature, value: number) => void | Promise<void>): void;
/**
* Send a Wake-on-LAN magic packet from the Gladys core network namespace
* (bridge containers cannot reach the LAN in broadcast). The core builds the
* standard fixed magic packet itself — this is not a general UDP proxy.
* Requires `network_wake: true` in the manifest (403 otherwise); 1 wake per
* 2 s per integration (429 beyond). A resolved call means the packet was
* emitted, not that the device actually woke up.
*/
wakeOnLan(mac: string, options?: WakeOnLanOptions): Promise<SuccessResponse>;

/**
* Handler called when the user actions a device feature (auto-acked).
* `value` is a number for every feature except the `text` category ones,
* whose commands are strings — the free text of a `text`/`text` feature,
* the selected option value of a `text`/`select` dynamic select.
*/
onSetValue(
callback: (device: Device, deviceFeature: DeviceFeature, value: number | string) => void | Promise<void>,
): void;

/** Handler called when the Gladys scheduler asks to poll a device (auto-acked). */
onPoll(callback: (device: Device) => void | Promise<void>): void;
Expand Down Expand Up @@ -1438,12 +1528,16 @@ export declare class GladysIntegration extends EventEmitter {
onHardwareUpdated(callback: (containers: HardwareUpdatedContainer[]) => void | Promise<void>): void;

/**
* Handler called when the user clicks "Connect" on an `oauth2` config field
* (auto-acked): build and return the provider authorization URL — client_id
* from the config, scopes, a `state` you generate and remember. The resolved
* string is acked as `data.authorize_url`.
* Handler called when the user clicks "Connect" on an `oauth2` or an
* `account_link` config field (auto-acked): build and return the provider
* authorization URL — for `oauth2`: client_id from the config, scopes, a
* `state` you generate and remember. The resolved string is acked as
* `data.authorize_url`. For an `account_link` field (a provider that never
* redirects back) `redirectUri` is `undefined` and there is no callback:
* watch for the approval yourself, then report it through
* `setConnectionStatus(true)`.
*/
onOAuthAuthorizeUrl(callback: (key: string, redirectUri: string) => string | Promise<string>): void;
onOAuthAuthorizeUrl(callback: (key: string, redirectUri: string | undefined) => string | Promise<string>): void;

/**
* Handler called when the OAuth2 provider redirects back (auto-acked):
Expand Down
71 changes: 71 additions & 0 deletions lib/device-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,26 @@ const DEVICE_FEATURE_CATEGORIES = {
ENERGY_SENSOR: 'energy-sensor',
ENERGY_PRODUCTION_SENSOR: 'energy-production-sensor',
FAN: 'fan',
GRID_SENSOR: 'grid-sensor',
HEATER: 'heater',
HEPA_FILTER_MONITORING: 'hepa-filter-monitoring',
HOME_OUTPUT_SENSOR: 'home-output-sensor',
HUMIDITY_SENSOR: 'humidity-sensor',
LEAK_SENSOR: 'leak-sensor',
LIGHT: 'light',
LIGHT_SENSOR: 'light-sensor',
LEVEL_SENSOR: 'level-sensor',
MOTION_SENSOR: 'motion-sensor',
LOCK: 'lock',
// Generic consumable/wear-part monitoring (vacuum brushes, dust bags, mop pads, softener resin,
// detergent...). One feature per component, the feature `name` identifies the component: don't
// add a new per-component type/category here when the value is just "remaining life in percent".
// Boundary with neighboring categories: filter life reported through the Matter Resource
// Monitoring model (HEPA and activated carbon filters) stays in HEPA_FILTER_MONITORING, every
// other consumable or wear part goes here, so the same quantity is never split across categories.
// The name `maintenance` is deliberate: it is a user-facing category name in the UI, kept broader
// and simpler than a Matter-style `consumable-monitoring`. Renaming it later would be breaking.
MAINTENANCE: 'maintenance',
MUSIC: 'music',
NOISE_SENSOR: 'noise-sensor',
OPENING_SENSOR: 'opening-sensor',
Expand All @@ -59,6 +70,16 @@ const DEVICE_FEATURE_CATEGORIES = {
PM25_SENSOR: 'pm25-sensor',
PM10_SENSOR: 'pm10-sensor',
FORMALDEHYD_SENSOR: 'formaldehyd-sensor',
// Gaseous air pollutants, one category per gas, holding the raw mass concentration measured in
// the air (µg/m³ by default, non-negative). Boundary with the neighbouring air quality
// categories: an index synthesizing several pollutants goes to `airquality-sensor`, and a
// protocol-specific severity level (Matter reports these gases as a 0-4 LevelValue, see
// `no2-matter-index-sensor`) is not a concentration and must not be published here. Whichever
// form the device natively reports is the one the integration maps, never both for the same
// measurement.
NO2_SENSOR: 'no2-sensor',
O3_SENSOR: 'o3-sensor',
SO2_SENSOR: 'so2-sensor',
PRECIPITATION_SENSOR: 'precipitation-sensor',
PRESENCE_SENSOR: 'presence-sensor',
PRESSURE_SENSOR: 'pressure-sensor',
Expand Down Expand Up @@ -134,6 +155,17 @@ const DEVICE_FEATURE_TYPES = {
},
CAMERA: {
IMAGE: 'image',
// PTZ control (spec docs/specs/camera-ptz-control.md). MOVE: one command feature for all
// movements, values from CAMERA_MOVE, per-camera subset declared via supported_options.
// PRESET: recall a saved position; the labeled list lives in supported_options, the value
// sent is the option's integer (the integration maps it to its protocol token).
// *_POSITION: optional absolute position, numeric read/write, bounds declared by the
// integration via min/max (units are integration-defined: normalized ONVIF space, degrees...).
MOVE: 'move',
PRESET: 'preset',
PAN_POSITION: 'pan-position',
TILT_POSITION: 'tilt-position',
ZOOM_POSITION: 'zoom-position',
},
CHARGING_STATION: {
CONNECTOR_STATUS: 'connector-status',
Expand All @@ -146,6 +178,7 @@ const DEVICE_FEATURE_TYPES = {
BINARY: 'binary',
LMH_VOLUME: 'lmh_volume',
MELODY: 'melody',
TEST_IN_PROGRESS: 'test-in-progress', // Alarm testing status (binary - sensor)
},
CHILD_LOCK: {
BINARY: 'binary',
Expand Down Expand Up @@ -259,12 +292,39 @@ const DEVICE_FEATURE_TYPES = {
THIRTY_MINUTES_CONSUMPTION_COST: 'thirty-minutes-consumption-cost',
},
ENERGY_PRODUCTION_SENSOR: {
POWER: 'power', // instantaneous production power, in W (>= 0)
INDEX: 'index',
DAILY_PRODUCTION: 'daily-production',
DAILY_PRODUCTION_REVENUE: 'daily-production-revenue',
THIRTY_MINUTES_PRODUCTION: 'thirty-minutes-production',
THIRTY_MINUTES_PRODUCTION_REVENUE: 'thirty-minutes-production-revenue',
},
// Exchange with the public grid (the connection point), whatever the
// measuring device: a plug-in battery's grid port, an EM clamp or a
// whole-home meter all publish here, so the same physical quantity never
// splits across categories. (teleinformation stays as-is for Linky legacy.)
// Import/export are split so core automations can tell direction apart;
// `power` is the signed single value some devices report instead
// (import > 0, export < 0). An integration maps whichever form its device
// NATIVELY reports - never both for the same measurement.
GRID_SENSOR: {
INPUT_POWER: 'input-power', // instantaneous power imported from the grid, W (>= 0)
OUTPUT_POWER: 'output-power', // instantaneous power exported to the grid, W (>= 0)
POWER: 'power', // signed grid exchange when the device reports a single value (import > 0, export < 0), W
INPUT_INDEX: 'input-index', // cumulative imported-energy meter index, kWh (>= 0)
OUTPUT_INDEX: 'output-index', // cumulative exported-energy meter index, kWh (>= 0)
},
// The power the device ITSELF delivers to the installation it feeds (e.g. a
// storage inverter's home output), plus its backup/off-grid output.
// House consumption measured by an inverter (the "load power" many hybrid
// inverters report, which can exceed the inverter's own output when the
// grid tops up) is NOT this category - it goes to energy-sensor.
HOME_OUTPUT_SENSOR: {
POWER: 'power', // instantaneous power delivered to the home, W (>= 0)
INDEX: 'index', // cumulative delivered-energy meter index, kWh (>= 0)
OFF_GRID_POWER: 'off-grid-power', // instantaneous power on the backup/off-grid output, W (>= 0)
OFF_GRID_INDEX: 'off-grid-index', // cumulative backup-output energy meter index, kWh (>= 0)
},
BATTERY_STORAGE: {
BATTERY_LEVEL: 'battery-level', // state of charge, % (0..100)
CHARGE_POWER: 'charge-power', // power INTO the battery, W/kW (>=0)
Expand Down Expand Up @@ -394,6 +454,14 @@ const DEVICE_FEATURE_TYPES = {
},
TEXT: {
TEXT: 'text',
// A choice among string values the integration discovers on the appliance itself
// (installed TV apps, HDMI sources, vacuum rooms, native scenes...): the choices are
// declared per-device through supported_options ({ value, label }) and are NOT part of
// the taxonomy. The state is the selected option's value, stored as a string
// (last_value_string, no history). Enum-like capabilities standards cover (AC modes,
// fan speeds...) keep their own category/type with integer values: this type is only
// for lists no generic value set can describe.
SELECT: 'select',
},
RISK: {
INTEGER: 'integer',
Expand Down Expand Up @@ -489,6 +557,9 @@ const DEVICE_FEATURE_TYPES = {
FILTER_MONITORING: {
FILTER_LIFE_REMAINING: 'filter-life-remaining', // Remaining life of the HEPA filter in percent (integer - sensor)
},
MAINTENANCE: {
LIFE_REMAINING: 'life-remaining', // Remaining life of a consumable/wear part in percent (integer - sensor)
},
VACUUM_CLEANER: {
STATE: 'state', // Operational state of the vacuum (integer - sensor)
RUN_MODE: 'run-mode', // Run mode of the vacuum (integer - command)
Expand Down
Loading
Loading