mirta 0.0.7 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,46 +1,407 @@
1
+ import { Event } from '@mirta/basics';
1
2
  export * from '@mirta/basics';
2
3
 
4
+ type DeviceType = 'wired' | 'virtual' | 'zigbee';
3
5
  /** Контекст устройства. */
4
6
  interface DeviceContext {
7
+ get deviceType(): DeviceType;
5
8
  /** Идентификатор устройства. */
6
- get id(): string;
9
+ get deviceId(): string;
7
10
  /**
8
11
  * Признак готовности к работе,
9
12
  * значение меняется при смене статуса. */
10
13
  get isReady(): boolean;
11
14
  }
12
15
  type TrackCallback = (controlId: string, callback: (value: WbRules.MqttValue) => void) => void;
13
- /** Контекст плагина для привязки к устройству. */
14
- interface PluginContext {
15
- device: DeviceContext;
16
- track: TrackCallback;
16
+
17
+ /**
18
+ * Определяет, имеет ли свойство строго заданное значение.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * type A = IsSpecified<{ value?: string }, 'value'> // A = false
23
+ * type B = IsSpecified<{ value: undefined }, 'value'> // B = false
24
+ * type C = IsSpecified<{ value: string }, 'value'> // C = true
25
+ * ```
26
+ * @since 0.1.0
27
+ */
28
+ type IsSpecified<TObject, TKey extends keyof TObject> = TObject extends Record<TKey, infer TValue> ? (TValue extends undefined ? false : true) : false;
29
+ /**
30
+ * На основе заданного свойства определяет, будет ли
31
+ * возвращаемый тип строгим.
32
+ *
33
+ * Используется, чтобы убрать неопределённость
34
+ * при наличии конкретного значения.
35
+ *
36
+ * Наиболее полно раскрывает себя в контексте вызова.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * interface Options {
41
+ * defaultValue?: string
42
+ * }
43
+ *
44
+ * function useComponent<TOptions extends Options>(
45
+ * _options?: TOptions
46
+ * ): StrictWhenSpecified<TOptions, 'defaultValue', string> {
47
+ *
48
+ * return void 0 as never
49
+ *
50
+ * }
51
+ *
52
+ * // Тип результата - string | undefined
53
+ *
54
+ * const r1 = useComponent()
55
+ * const r2 = useComponent({ defaultValue: undefined })
56
+ *
57
+ * // Тип результата - string
58
+ *
59
+ * const r3 = useComponent({ defaultValue: '' })
60
+ * ```
61
+ * @since 0.1.0
62
+ **/
63
+ type StrictWhenSpecified<TObject, TKey extends keyof TObject, TReturn> = IsSpecified<TObject, TKey> extends true ? TReturn : TReturn | undefined;
64
+ /**
65
+ * Устанавливает указанное свойство в readonly при выполнении указанного условия.
66
+ * @since 0.1.0
67
+ **/
68
+ type ReadonlyPropWhen<TObject, K extends keyof TObject, TCondition extends boolean | undefined> = TCondition extends true ? Omit<TObject, K> & {
69
+ +readonly [P in K]-?: TObject[P];
70
+ } : TObject;
71
+ /**
72
+ * Проверяет, что указанный тип объекта имеет хотя бы одно свойство заданного типа.
73
+ * Применяется для работы с дженериками.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * type A = { propA: { required: true }, propB: { required: false } }
78
+ * type Y = HasPropertyOfType<A, { required: true }> // Y = true
79
+ *
80
+ * type B = { propA: { required: false }, propB: { required: false } }
81
+ * type N = HasPropertyOfType<B, { required: true }> // N = false
82
+ * ```
83
+ * @since 0.1.0
84
+ **/
85
+ type HasPropertyOfType<TObject extends object, TProperty> = {
86
+ [K in keyof TObject as TObject[K] extends TProperty ? K : never]: unknown;
87
+ } extends infer R ? {} extends R ? false : true : never;
88
+
89
+ type ValueEventHandler<TValue> = (newValue: TValue, oldValue: TValue) => void;
90
+ interface Control<TValue> {
91
+ /** Актуальное значение контрола. */
92
+ value: TValue;
93
+ /** Событие, происходящее когда поступило новое значение. */
94
+ valueReceived: Event<ValueEventHandler<TValue>>;
95
+ /** Событие, происходящее когда значение изменилось. */
96
+ valueChanged: Event<ValueEventHandler<TValue>>;
17
97
  }
18
- /** Плагин для расширения функциональности устройства. */
19
- type DevicePlugin<TPlugin = unknown> = (context: PluginContext) => TPlugin;
98
+ type MaybeReadonlyControl<TValue, TReadonly extends boolean | undefined> = ReadonlyPropWhen<Control<TValue>, 'value', TReadonly>;
20
99
 
21
- interface DeviceSetupOptions {
22
- context: DeviceContext;
23
- setControl: (controlId: string, definition: WbRules.ControlOptions) => void;
100
+ /**
101
+ * Определяет соответствие между свойствами
102
+ * {@link ControlType.type} и {@link ControlType.defaultValue}.
103
+ *
104
+ **/
105
+ interface ControlType<TControl, TValue> {
106
+ /** Тип контрола. */
107
+ type: TControl;
108
+ /** Значение по умолчанию. */
109
+ defaultValue?: TValue;
24
110
  }
25
111
  /**
26
- * Извлекает типы возвращаемых функциями в массиве значений
27
- * в виде пересечения.
112
+ * Служит механизмом безопасного преобразования типов контролов
113
+ * в конкретные типы значений.
114
+ **/
115
+ type TypeMapper<K extends keyof WbRules.TypeMappings = keyof WbRules.TypeMappings> = K extends infer TControl ? ControlType<TControl, WbRules.TypeMappings[K]> : never;
116
+ type VirtualTypeMapper<K extends keyof WbRules.TypeMappings = keyof WbRules.TypeMappings> = K extends infer TControl ? (ControlType<TControl, WbRules.TypeMappings[K]> & WbRules.ControlTypeExtension<TControl>) : never;
117
+ interface BaseControlDef {
118
+ /** Идентификатор контрола. Если не указан, используется название свойства. */
119
+ controlId?: string;
120
+ /** Если `true`, то значение доступно только для чтения. */
121
+ isReadonly?: boolean;
122
+ }
123
+ /**
124
+ * Определения основных контролов.
125
+ * Расширяют возможные типы контролов поддержкой общих полей.
126
+ **/
127
+ type ControlDef = Expand<TypeMapper & BaseControlDef>;
128
+ /**
129
+ * Определения виртуальных контролов.
130
+ * Расширяют определения основных контролов поддержкой дополнительных полей,
131
+ * характерных только для виртуальных устройств.
132
+ **/
133
+ type VirtualControlDef = Expand<VirtualTypeMapper & BaseControlDef & {
134
+ /** Имя, публикуемое в MQTT-топике */
135
+ title?: WbRules.Title;
136
+ /** Если включено, устанавливает значение по умолчанию при перезапуске. */
137
+ forceDefault?: boolean;
138
+ /** Если включено, не создаёт контрол в MQTT, пока ему не будет присвоено какое-либо значение. */
139
+ lazyInit?: boolean;
140
+ /** Порядок следования полей */
141
+ order?: number;
142
+ }>;
143
+ /** Набор контролов, не поддерживающий переопределение. */
144
+ type Controls = Record<string, ControlDef>;
145
+ /** Набор контролов с поддержкой переопределения. */
146
+ type VirtualControls = Record<string, VirtualControlDef>;
147
+ /**
148
+ * Обеспечивает строго контролируемую вариативность в зависимости
149
+ * от контекста вызова.
150
+ *
151
+ * Проверяет совместимость с прототипом `Controls`,
152
+ * предотвращая появление неучтённых свойств (например тех,
153
+ * что используются только для виртуальных устройств).
154
+ **/
155
+ type StrictControls<TControls extends Controls> = TControls & Controls;
156
+ /**
157
+ * Обеспечивает строго контролируемую вариативность в зависимости
158
+ * от контекста вызова.
159
+ *
160
+ * Проверяет совместимость с прототипом `VirtualControls`,
161
+ * предотвращая появление неучтённых свойств.
162
+ **/
163
+ type StrictVirtualControls<TControls extends VirtualControls> = TControls & VirtualControls;
164
+ /**
165
+ * Набор готовых к использованию контролов, передаётся в `setup()`
166
+ * при описании устройства в `defineDevice()`.
167
+ **/
168
+ type CreatedControls<TControls extends Controls | VirtualControls> = {
169
+ [K in keyof TControls]: Expand<MaybeReadonlyControl<StrictWhenSpecified<TControls[K], 'defaultValue', WbRules.TypeMappings[TControls[K]['type']]>, TControls[K]['isReadonly']>>;
170
+ };
171
+ /** Используется для извлечения типа значения из значения по умолчанию. */
172
+ type InferDefaultType<TProp> = [TProp] extends [{
173
+ defaultValue: infer TDefault;
174
+ }] ? TDefault extends (() => infer TReturn) ? TReturn : TDefault : TProp;
175
+ /** Извлекает реальный тип свойства из его определения. */
176
+ type InferPropType<TProp> = [TProp] extends [ObjectConstructor | {
177
+ type: ObjectConstructor;
178
+ }] ? Record<string, unknown> : [TProp] extends [BooleanConstructor | {
179
+ type: BooleanConstructor;
180
+ }] ? boolean : [TProp] extends [NumberConstructor | {
181
+ type: NumberConstructor;
182
+ }] ? number : [TProp] extends [StringConstructor | {
183
+ type: StringConstructor;
184
+ }] ? string : [TProp] extends [DateConstructor | {
185
+ type: DateConstructor;
186
+ }] ? Date : [TProp] extends [PropType<infer TValue> | {
187
+ type: PropType<infer TValue>;
188
+ } | undefined] ? TValue : InferDefaultType<TProp>;
189
+ /** Обязательные ключи свойств - потребуют их явного указания при вызове useDevice(). */
190
+ type RequiredKeys<TProps> = {
191
+ [K in keyof TProps]: TProps[K] extends {
192
+ isRequired: true;
193
+ } ? TProps[K] extends {
194
+ defaultValue: undefined | (() => undefined);
195
+ } ? never : K : never;
196
+ }[keyof TProps];
197
+ /**
198
+ * Опциональные ключи свойств - указываются по необходимости.
28
199
  *
29
- * */
30
- type IntersectReturnTypes<TArray> = TArray extends [(...args: unknown[]) => infer TReturn, ...infer TRest] ? TReturn & IntersectReturnTypes<TRest> : object;
31
- interface DefineDeviceOptions<TPlugins extends DevicePlugin[]> {
32
- setup?: (options: DeviceSetupOptions) => void;
33
- plugins?: [...TPlugins];
200
+ * Рекомендуется использовать совместно с параметром {@link PropMetadata.defaultValue}.
201
+ */
202
+ type OptionalKeys<TProps> = Exclude<keyof TProps, RequiredKeys<TProps>>;
203
+ type RequiredSetupKeys<TProps> = {
204
+ [K in keyof TProps]: TProps[K] extends {
205
+ isRequired: true;
206
+ } | {
207
+ defaultValue: unknown;
208
+ } ? TProps[K] extends {
209
+ defaultValue: undefined | (() => undefined);
210
+ } ? never : K : never;
211
+ }[keyof TProps];
212
+ type OptionalSetupKeys<TProps> = Exclude<keyof TProps, RequiredKeys<TProps>>;
213
+ /**
214
+ * Параметры устройства для уточнения стартовой конфигурации.
215
+ *
216
+ * Например, можно передать значения низкого и критического уровня заряда, определяя контрол батареи.
217
+ */
218
+ type Props<TProps> = {
219
+ [K in keyof Pick<TProps, RequiredKeys<TProps>>]: InferPropType<TProps[K]>;
220
+ } & {
221
+ [K in keyof Pick<TProps, OptionalKeys<TProps>>]?: InferPropType<TProps[K]>;
222
+ };
223
+ /**
224
+ * В отличие от {@link Props}, формирует строгое значение свойства
225
+ * при заданном параметре {@link PropMetadata.defaultValue}.
226
+ *
227
+ **/
228
+ type SetupProps<TProps> = {
229
+ [K in keyof Pick<TProps, RequiredSetupKeys<TProps>>]: Readonly<InferPropType<TProps[K]>>;
230
+ } & {
231
+ [K in keyof Pick<TProps, OptionalSetupKeys<TProps>>]?: Readonly<InferPropType<TProps[K]>>;
232
+ };
233
+ type PropMethod<TProp, TConstructor = unknown> = [TProp] extends [
234
+ ((...args: unknown[]) => unknown) | undefined
235
+ ] ? {
236
+ new (): TConstructor;
237
+ (): TProp;
238
+ readonly prototype: TConstructor;
239
+ } : never;
240
+ type PropConstructor<TProp = unknown> = (new (...args: unknown[]) => TProp & {}) | (() => TProp) | PropMethod<TProp>;
241
+ /**
242
+ * Тип параметра контрола.
243
+ * @example
244
+ * ```ts
245
+ * const useDevice = defineWiredDevice({
246
+ * props: {
247
+ * // Неявное использование PropType для извлечения типа.
248
+ * count: Number
249
+ * }
250
+ * })
251
+ * ```
252
+ * @example
253
+ * ```ts
254
+ * interface BatteryProps {
255
+ * lowLevel: number
256
+ * criticalLevel: number
257
+ * }
258
+ *
259
+ * const useDevice = defineWiredDevice({
260
+ * props: {
261
+ * // Явное использование PropType для извлечения типа.
262
+ * battery: Object as PropType<BatteryProps>
263
+ * }
264
+ * })
265
+ * ```
266
+ **/
267
+ type PropType<TProp> = PropConstructor<TProp> | (PropConstructor<TProp>)[];
268
+ /**
269
+ * Метаданные свойства.
270
+ *
271
+ * Применяются для уточнения параметров свойства.
272
+ *
273
+ * @example
274
+ * Делает свойство обязательным.
275
+ *
276
+ * ```ts
277
+ * const useDevice = defineWiredDevice({
278
+ * props: {
279
+ * count: { type: Number, isRequired: true }
280
+ * }
281
+ * })
282
+ *
283
+ * const devie = useDevice('my_device_id', {
284
+ * count: 0
285
+ * })
286
+ * ```
287
+ * @example
288
+ * Делает свойство опциональным, но строгим - предоставляет значение по умолчанию.
289
+ *
290
+ * ```ts
291
+ * const useDevice = defineWiredDevice({
292
+ * props: {
293
+ * count: { type: Number, defaultValue: 0 }
294
+ * }
295
+ * })
296
+ *
297
+ * const device = useDevice('my_device_id')
298
+ * ```
299
+ **/
300
+ interface PropMetadata<TProp> {
301
+ /** Тип свойства. */
302
+ type: TProp;
303
+ /** Признак обязательности. */
304
+ isRequired?: boolean;
305
+ /** Значение по умолчанию. */
306
+ defaultValue?: TProp | (() => TProp);
34
307
  }
35
- type DeviceWithPlugins<TPlugins extends DevicePlugin[]> = IntersectReturnTypes<[...TPlugins]>;
308
+ /**
309
+ * Обеспечивает поддержку одновременно компактной и расширенной записи
310
+ * определений начальных настроек устройства.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * const useDevice = defineWiredDevice({
315
+ * props: {
316
+ * // Компактная форма.
317
+ * count: Number,
318
+ * // Расширенная форма.
319
+ * checkInterval: { type: Number, defaultValue: 100 }
320
+ * }
321
+ * })
322
+ * ```
323
+ */
324
+ type Prop<TProp> = PropMetadata<TProp> | PropType<TProp>;
325
+ type PropsDefinition<TProps = Record<string, unknown>> = {
326
+ [K in keyof TProps]: Prop<TProps[K]>;
327
+ };
328
+ /** Определение проводного устройства. */
329
+ interface WiredDeviceOptions<TProps, TControls extends Controls, TResult> {
330
+ setup: (options: {
331
+ props: Readonly<Expand<SetupProps<TProps>>>;
332
+ controls: CreatedControls<TControls>;
333
+ }) => TResult;
334
+ props?: TProps;
335
+ /** Контролы проводного устройства. */
336
+ controls: StrictControls<TControls>;
337
+ }
338
+ /** Определение виртуального устройства. */
339
+ interface VirtualDeviceOptions<TProps, TControls extends VirtualControls, TResult> {
340
+ setup: (options: {
341
+ props: Readonly<Expand<SetupProps<TProps>>>;
342
+ controls: CreatedControls<TControls>;
343
+ }) => TResult;
344
+ props?: TProps;
345
+ /** Контролы виртуального устройства. */
346
+ controls: StrictVirtualControls<TControls>;
347
+ }
348
+ /** Определение Zigbee-устройства. */
349
+ interface ZigbeeDeviceOptions<TProps, TControls extends VirtualControls, TResult> {
350
+ setup: (options: {
351
+ props: Readonly<Expand<SetupProps<TProps>>>;
352
+ controls: CreatedControls<TControls>;
353
+ }) => TResult;
354
+ props?: TProps;
355
+ /** Контролы Zigbee-устройства. */
356
+ controls: StrictVirtualControls<TControls>;
357
+ }
358
+ /**
359
+ * Описывает конфигурацию устройства в зависимости от его типа - проводное, виртуальное или Zigbee.
360
+ *
361
+ * @template TDeviceType Тип устройства (проводное, виртуальное, беспроводное).
362
+ * @template TProps Тип списка с определениями входящих параметров.
363
+ * @template TControls Тип списка с определениями контролов устройства.
364
+ * @template TResult Итоговый тип устройства.
365
+ */
366
+ type DeviceOptions<TDeviceType extends DeviceType, TProps, TControls extends Controls | VirtualControls, TResult> = TDeviceType extends 'wired' ? WiredDeviceOptions<TProps, TControls, TResult> : (TDeviceType extends 'virtual' ? VirtualDeviceOptions<TProps, TControls, TResult> : (TDeviceType extends 'zigbee' ? ZigbeeDeviceOptions<TProps, TControls, TResult> : never));
367
+ /**
368
+ * Наделяет результирующее устройство возможностью передачи своего контекста.
369
+ *
370
+ * Используется во вспомогательных функциях, избавляя от необходимости прописывать идентификаторы вручную.
371
+ *
372
+ **/
36
373
  interface DeviceWithContext {
37
- /** Контекст устройства. */
38
- context: DeviceContext;
39
- /** Метод для отслеживания изменений в топиках. */
40
- track: (controlId: string, callback: (newValue: WbRules.MqttValue) => void) => void;
374
+ /** Контекст устройства - ключевая информация. */
375
+ context: {
376
+ /** Идентификатор устройства. */
377
+ deviceId: string;
378
+ /** Признак готовности к работе. */
379
+ isReady: boolean;
380
+ };
381
+ }
382
+ /** Итоговый тип устройства. */
383
+ type Device<TDefinition> = TDefinition & DeviceWithContext;
384
+ type UseDeviceFunc<TProps extends object, TDevice> = HasPropertyOfType<TProps, {
385
+ isRequired: true;
386
+ }> extends true ? (deviceId: string, props: Expand<Props<TProps>>) => Device<TDevice> : (deviceId: string, props?: Expand<Props<TProps>>) => Device<TDevice>;
387
+ /**
388
+ * Определяет структуру проводного устройства.
389
+ * @since 0.1.0
390
+ **/
391
+ declare function defineWiredDevice<TDeviceType extends DeviceType, TProps extends PropsDefinition, TControls extends Controls | VirtualControls, TDevice extends object>(options: DeviceOptions<TDeviceType, TProps, TControls, TDevice>): UseDeviceFunc<TProps, TDevice>;
392
+ interface WithIntegratedTitleProp {
393
+ title?: PropMetadata<PropType<WbRules.Title>> | PropType<WbRules.Title>;
41
394
  }
42
- type Device<TPlugins extends DevicePlugin[]> = DeviceWithContext & DeviceWithPlugins<TPlugins>;
43
- declare function defineZigbeeDevice<TPlugins extends DevicePlugin[]>(deviceId: string, options?: DefineDeviceOptions<TPlugins>): Device<TPlugins>;
395
+ /**
396
+ * Определяет структуру виртуального устройства.
397
+ * @since 0.1.0
398
+ **/
399
+ declare function defineVirtualDevice<TDeviceType extends DeviceType, TProps extends PropsDefinition, TControls extends Controls | VirtualControls, TDevice extends object>(options: DeviceOptions<TDeviceType, TProps & WithIntegratedTitleProp, TControls, TDevice>): UseDeviceFunc<TProps & WithIntegratedTitleProp, TDevice>;
400
+ /**
401
+ * Определяет структуру zigbee-устройства.
402
+ * @since 0.1.0
403
+ **/
404
+ declare function defineZigbeeDevice<TDeviceType extends DeviceType, TProps extends PropsDefinition, TControls extends Controls | VirtualControls, TDevice extends object>(options: DeviceOptions<TDeviceType, TProps, TControls, TDevice>): UseDeviceFunc<TProps, TDevice>;
44
405
 
45
406
  /**
46
407
  * Позволяет получить объект для работы с указанным устройством.
@@ -70,5 +431,5 @@ declare function getControlSafe(context: DeviceContext, controlId: string): Cont
70
431
  **/
71
432
  declare function getControlSafe(deviceId: string, controlId: string, isReadyFunc: () => boolean): ControlSafe;
72
433
 
73
- export { defineZigbeeDevice, getControlSafe, getDeviceSafe };
74
- export type { ControlSafe, DeviceContext, DevicePlugin, PluginContext, TrackCallback };
434
+ export { defineVirtualDevice, defineWiredDevice, defineZigbeeDevice, getControlSafe, getDeviceSafe };
435
+ export type { ControlSafe, DeviceContext, DeviceType, PropType, TrackCallback };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { useEvent, isFunction } from '@mirta/basics';
1
2
  export * from '@mirta/basics';
2
3
  import '@mirta/polyfills';
3
4
 
@@ -15,21 +16,123 @@ function getControlSafe(context, controlId, isReadyFunc = () => true) {
15
16
  }
16
17
  else {
17
18
  return context.isReady
18
- ? control = getControl(`${context.id}/${controlId}`)
19
+ ? control = getControl(`${context.deviceId}/${controlId}`)
19
20
  : undefined;
20
21
  }
21
22
  },
22
23
  };
23
24
  }
24
25
 
26
+ const typeMappings = {
27
+ 'text': 'string',
28
+ 'value': 'number',
29
+ 'switch': 'boolean',
30
+ 'pushbutton': 'boolean',
31
+ 'rgb': 'string',
32
+ 'range': 'number',
33
+ 'alarm': 'boolean',
34
+ };
35
+ /**
36
+ * Создаёт контрол устройства.
37
+ * @since 0.1.0
38
+ **/
39
+ function createControl(context, controlId, options) {
40
+ const { deviceType, deviceId } = context;
41
+ const { type, isReadonly } = options;
42
+ const defaultValue = 'defaultValue' in options
43
+ ? options['defaultValue']
44
+ : void 0;
45
+ const control = getControlSafe(context, controlId);
46
+ /** Отправляет новое значение в контрол устройства. */
47
+ const emitValue = (newValue) => {
48
+ if (newValue === void 0)
49
+ return;
50
+ if (deviceType === 'zigbee') {
51
+ publish(`zigbee2mqtt/${deviceId}/set`, JSON.stringify({ [controlId]: newValue }));
52
+ }
53
+ else {
54
+ control.safe?.setValue(newValue);
55
+ }
56
+ };
57
+ const valueReceived = useEvent();
58
+ const valueChanged = useEvent();
59
+ let localValue;
60
+ /**
61
+ * Устанавливает новое значение, если оно отличается от существующего.
62
+ * @param newValue Устанавливаемое значение.
63
+ * @param preventEmit Предотвращает отправку значения в устройство.
64
+ **/
65
+ function setValue(newValue, preventEmit = false) {
66
+ const oldValue = localValue;
67
+ valueReceived.raise(newValue, oldValue);
68
+ if (oldValue === newValue)
69
+ return;
70
+ localValue = newValue;
71
+ if (!preventEmit)
72
+ emitValue(newValue);
73
+ valueChanged.raise(newValue, oldValue);
74
+ }
75
+ trackMqtt(`/devices/${deviceId}/controls/${controlId}`, (payload) => {
76
+ const incomingType = typeof payload.value;
77
+ const expectingType = typeMappings[type];
78
+ if (incomingType === 'string' && expectingType === 'number') {
79
+ const value = Number(payload.value);
80
+ if (isNaN(value)) {
81
+ log.error(`Value ignored: control '${deviceId}/${controlId}' expects number, but Number(value) is NaN.`);
82
+ return;
83
+ }
84
+ setValue(value, true);
85
+ }
86
+ else {
87
+ if (incomingType !== expectingType) {
88
+ log.error(`Value ignored: control '${deviceId}/${controlId}' expects type '${expectingType}', but received '${incomingType}'`);
89
+ return;
90
+ }
91
+ setValue(payload.value, true);
92
+ }
93
+ });
94
+ return {
95
+ get value() {
96
+ if (localValue === undefined) {
97
+ if (options.forceDefault)
98
+ return localValue = defaultValue;
99
+ if (!options.lazyInit)
100
+ return localValue ??= control.safe?.getValue();
101
+ }
102
+ return localValue;
103
+ },
104
+ set value(newValue) {
105
+ if (isReadonly) {
106
+ log.warning(`Value of '${deviceId}/${controlId}' is readonly`);
107
+ return;
108
+ }
109
+ setValue(newValue);
110
+ },
111
+ valueReceived: valueReceived
112
+ .withoutRaise(),
113
+ valueChanged: valueChanged
114
+ .withoutRaise(),
115
+ };
116
+ }
117
+
25
118
  const { assign } = Object;
26
- if ((process.env.NODE_ENV === 'test')) {
119
+ if ((process.env.NODE_ENV === 'test'))
27
120
  module.static = {};
28
- }
29
- const configured = (module.static.configured ??= {});
30
- function createContext(deviceId, state) {
121
+ /**
122
+ * Содержит список проинициализированных устройств,
123
+ * а также их статус готовности к работе.
124
+ *
125
+ * Например, если какой-либо из скриптов создал виртуальное устройство,
126
+ * другие скрипты не должны пытаться создать его повторно.
127
+ *
128
+ **/
129
+ const alreadyConfigured = (module.static.configured ??= {});
130
+ function createContext(deviceType, deviceId, state) {
31
131
  const context = {
32
- get id() {
132
+ get deviceType() {
133
+ return deviceType;
134
+ },
135
+ get deviceId() {
33
136
  return deviceId;
34
137
  },
35
138
  get isReady() {
@@ -38,70 +141,168 @@ function createContext(deviceId, state) {
38
141
  };
39
142
  return context;
40
143
  }
41
- function configureContext(deviceId, setup) {
42
- // Каждый юнит-тест должен проходить полный цикл построения.
144
+ function configureControls(deviceId, controls) {
145
+ const device = getDevice(deviceId);
146
+ if (!device)
147
+ return;
148
+ Object.keys(controls).forEach((key) => {
149
+ const control = controls[key];
150
+ const controlId = control.controlId ?? key;
151
+ if ((process.env.NODE_ENV !== 'production'))
152
+ log.debug(`Replacing the control '${controlId}' on '${deviceId}'`);
153
+ if (device.isControlExists(controlId))
154
+ device.removeControl(controlId);
155
+ device.addControl(controlId, assign({}, control, {
156
+ value: control.defaultValue,
157
+ readonly: control.isReadonly,
158
+ }));
159
+ });
160
+ }
161
+ function configureContext(type, deviceId, controls, title) {
43
162
  if ((process.env.NODE_ENV === 'test'))
44
- configured[deviceId] = undefined;
45
- if (configured[deviceId])
46
- return createContext(deviceId, configured[deviceId]);
47
- const state = configured[deviceId] = {
163
+ alreadyConfigured[deviceId] = undefined;
164
+ if (alreadyConfigured[deviceId])
165
+ return createContext(type, deviceId, alreadyConfigured[deviceId]);
166
+ const state = alreadyConfigured[deviceId] = {
48
167
  isReady: false,
49
168
  isConfigurable: true,
50
169
  };
51
- const context = createContext(deviceId, state);
52
- trackMqtt(`zigbee2mqtt/${deviceId}`, () => {
53
- // Предотвращает проверку каждого сообщения от zigbee2mqtt
54
- if (state.isReady || !state.isConfigurable)
55
- return;
56
- const device = getDevice(deviceId);
57
- if (setup && device?.isVirtual()) {
58
- setup({
59
- context,
60
- setControl(controlId, description) {
61
- if (device.isControlExists(controlId))
62
- device.removeControl(controlId);
63
- if ((process.env.NODE_ENV !== 'production'))
64
- log.debug(`Replacing the control '${controlId}' on '${deviceId}'`);
65
- device.addControl(controlId, description);
66
- },
67
- });
170
+ const context = createContext(type, deviceId, state);
171
+ if (state.isReady || !state.isConfigurable)
172
+ return;
173
+ if (type === 'zigbee') {
174
+ trackMqtt(`zigbee2mqtt/${deviceId}`, () => {
175
+ if (state.isReady || !state.isConfigurable)
176
+ return;
177
+ configureControls(deviceId, controls);
68
178
  state.isReady = true;
179
+ });
180
+ }
181
+ else {
182
+ if (type === 'virtual') {
183
+ const cells = {};
184
+ Object.keys(controls).forEach((key) => {
185
+ const cell = assign({}, controls[key], {
186
+ value: controls[key]['defaultValue'],
187
+ }, controls[key]['isReadonly'] ? { readonly: true } : {});
188
+ cells[key] = cell;
189
+ });
190
+ global.defineVirtualDevice(deviceId, {
191
+ title: title ?? 'Untitled Virtual Device',
192
+ cells,
193
+ });
69
194
  }
70
- else {
71
- state.isConfigurable = false;
72
- if ((process.env.NODE_ENV !== 'production'))
73
- log.warning(`Can't configure '${deviceId}' as a zigbee-device`);
74
- }
75
- });
195
+ state.isReady = true;
196
+ }
76
197
  return context;
77
198
  }
78
- function defineZigbeeDevice(deviceId, options = {}) {
79
- const context = configureContext(deviceId, options.setup);
80
- const lastSeen = getControlSafe(context, 'last_seen');
81
- const startupStamp = lastSeen.safe?.getValue();
82
- function track(controlId, callback) {
83
- trackMqtt(`/devices/${deviceId}/controls/${controlId}`, (payload) => {
84
- const stamp = lastSeen.safe?.getValue();
85
- if (stamp === startupStamp)
86
- return;
87
- callback(payload.value);
199
+ function getValueOrDefault(value, defaultValue) {
200
+ return value ?? (isFunction(defaultValue)
201
+ ? defaultValue()
202
+ : defaultValue);
203
+ }
204
+ function createDevice(deviceType, deviceId, propDefs, props, options) {
205
+ const controlDefs = options.controls;
206
+ const setupProps = {};
207
+ const controls = {};
208
+ Object.keys(propDefs).forEach((key) => {
209
+ // Устанавливает значение свойства по умолчанию,
210
+ // если не указано иного.
211
+ //
212
+ setupProps[key] = getValueOrDefault(props[key], propDefs[key]['defaultValue']);
213
+ });
214
+ const context = configureContext(deviceType, deviceId, controlDefs, deviceType === 'virtual'
215
+ ? setupProps['title']
216
+ : '');
217
+ Object.keys(controlDefs).forEach((key) => {
218
+ const controlDef = controlDefs[key];
219
+ const controlId = controlDef.controlId ?? key;
220
+ const control = createControl({ deviceType, deviceId, isReady: true }, controlId, {
221
+ type: controlDef.type,
222
+ defaultValue: controlDef.defaultValue,
223
+ isReadonly: controlDef.isReadonly,
224
+ forceDefault: controlDef.forceDefault,
225
+ lazyInit: controlDef.lazyInit,
88
226
  });
89
- }
90
- const device = {
91
- context,
92
- track,
227
+ controls[key] = control;
228
+ });
229
+ const device = options.setup({
230
+ props: setupProps,
231
+ controls: controls,
232
+ });
233
+ return assign(device, { context });
234
+ }
235
+ /**
236
+ * В пределах каждого скрипта wb-rules (точки входа),
237
+ * содержит синглтоны построенных в нём устройств соответственно
238
+ * переданному в `useDevice()` идентификатору.
239
+ *
240
+ */
241
+ const devices = {};
242
+ function defineDevice(type, options) {
243
+ return (deviceId, props) => {
244
+ // Девайс должен проходить полный цикл построения в юнит-тестах.
245
+ if ((process.env.NODE_ENV === 'test'))
246
+ devices[deviceId] = void 0;
247
+ return (
248
+ // Строится единожды на скрипт, далее берётся из кэша.
249
+ // При повторном вызове переданные свойства будут проигнорированы.
250
+ devices[deviceId] ??= createDevice(type, deviceId, options.props ?? {}, props ?? {}, options));
93
251
  };
94
- const plugins = options.plugins;
95
- if (plugins) {
96
- const pluginContext = {
97
- device: context,
98
- track,
99
- };
100
- options.plugins?.forEach((plugin) => {
101
- assign(device, plugin(pluginContext));
102
- });
103
- }
104
- return device;
252
+ }
253
+ /**
254
+ * Определяет структуру проводного устройства.
255
+ * @since 0.1.0
256
+ **/
257
+ function defineWiredDevice(options) {
258
+ return defineDevice('wired', options);
259
+ }
260
+ let virtualPerScript = 0;
261
+ /**
262
+ * Обеспечивает нумерацию безымянных виртуальных
263
+ * устройств в пределах скрипта.
264
+ *
265
+ **/
266
+ const getNextVirtualNumber = () => virtualPerScript += 1;
267
+ /**
268
+ * Использует {@link getNextVirtualNumber} и возвращает
269
+ * номер устройства, дополненный ведущими нулями.
270
+ *
271
+ * @returns Дополненная ведущими нулями строка с номером.
272
+ *
273
+ **/
274
+ function getNextVirtualNumberPadded() {
275
+ const nextNumber = String(getNextVirtualNumber());
276
+ if (nextNumber.length < 2)
277
+ return '00' + nextNumber;
278
+ if (nextNumber.length < 3)
279
+ return '0' + nextNumber;
280
+ return nextNumber;
281
+ }
282
+ /**
283
+ * Определяет структуру виртуального устройства.
284
+ * @since 0.1.0
285
+ **/
286
+ function defineVirtualDevice(options) {
287
+ // Виртуальные устройства определяют заголовок по умолчанию,
288
+ // если он не передан во входящих параметрах.
289
+ //
290
+ options.props = assign({}, {
291
+ title: {
292
+ type: Object, defaultValue: () => {
293
+ const match = /\/([^/]+?)(?:\.js)?$/.exec(__filename);
294
+ return `Virtual #${getNextVirtualNumberPadded()}${match?.[1] ? ` at '${match[1]}'` : ''}`;
295
+ },
296
+ },
297
+ }, options.props);
298
+ return defineDevice('virtual', options);
299
+ }
300
+ /**
301
+ * Определяет структуру zigbee-устройства.
302
+ * @since 0.1.0
303
+ **/
304
+ function defineZigbeeDevice(options) {
305
+ return defineDevice('zigbee', options);
105
306
  }
106
307
 
107
308
  /**
@@ -121,4 +322,4 @@ function getDeviceSafe(deviceId, isReadyFunc = () => true) {
121
322
  };
122
323
  }
123
324
 
124
- export { defineZigbeeDevice, getControlSafe, getDeviceSafe };
325
+ export { defineVirtualDevice, defineWiredDevice, defineZigbeeDevice, getControlSafe, getDeviceSafe };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mirta",
3
3
  "description": "The powerful framework to write smart home automation scripts.",
4
- "version": "0.0.7",
4
+ "version": "0.1.1",
5
5
  "license": "Unlicense",
6
6
  "keywords": [
7
7
  "mirta",
@@ -36,9 +36,9 @@
36
36
  "url": "https://pay.cloudtips.ru/p/58512cca"
37
37
  },
38
38
  "dependencies": {
39
- "@mirta/basics": "0.0.7",
40
- "@mirta/globals": "0.0.7",
41
- "@mirta/polyfills": "0.0.7"
39
+ "@mirta/basics": "0.1.1",
40
+ "@mirta/polyfills": "0.1.1",
41
+ "@mirta/globals": "0.1.1"
42
42
  },
43
43
  "publishConfig": {
44
44
  "access": "public"