mirta 0.0.6 → 0.1.0
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 +387 -26
- package/dist/index.mjs +260 -61
- package/package.json +4 -4
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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:
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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,
|
|
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.
|
|
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 value;
|
|
60
|
+
/**
|
|
61
|
+
* Устанавливает новое значение, если оно отличается от существующего.
|
|
62
|
+
* @param newValue Устанавливаемое значение.
|
|
63
|
+
* @param preventEmit Предотвращает отправку значения в устройство.
|
|
64
|
+
**/
|
|
65
|
+
function setValue(newValue, preventEmit = false) {
|
|
66
|
+
const oldValue = value;
|
|
67
|
+
valueReceived.raise(newValue, oldValue);
|
|
68
|
+
if (oldValue === newValue)
|
|
69
|
+
return;
|
|
70
|
+
value = 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 (value === undefined) {
|
|
97
|
+
if (options.forceDefault)
|
|
98
|
+
return value = defaultValue;
|
|
99
|
+
if (!options.lazyInit)
|
|
100
|
+
return value ??= control.safe?.getValue();
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
132
|
+
get deviceType() {
|
|
133
|
+
return deviceType;
|
|
134
|
+
},
|
|
135
|
+
get deviceId() {
|
|
33
136
|
return deviceId;
|
|
34
137
|
},
|
|
35
138
|
get isReady() {
|
|
@@ -38,70 +141,166 @@ function createContext(deviceId, state) {
|
|
|
38
141
|
};
|
|
39
142
|
return context;
|
|
40
143
|
}
|
|
41
|
-
function
|
|
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
|
-
|
|
45
|
-
if (
|
|
46
|
-
return createContext(deviceId,
|
|
47
|
-
const state =
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
88
224
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
225
|
+
controls[key] = control;
|
|
226
|
+
});
|
|
227
|
+
const device = options.setup({
|
|
228
|
+
props: setupProps,
|
|
229
|
+
controls: controls,
|
|
230
|
+
});
|
|
231
|
+
return assign(device, { context });
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* В пределах каждого скрипта wb-rules (точки входа),
|
|
235
|
+
* содержит синглтоны построенных в нём устройств соответственно
|
|
236
|
+
* переданному в `useDevice()` идентификатору.
|
|
237
|
+
*
|
|
238
|
+
*/
|
|
239
|
+
const devices = {};
|
|
240
|
+
function defineDevice(type, options) {
|
|
241
|
+
return (deviceId, props) => {
|
|
242
|
+
// Девайс должен проходить полный цикл построения в юнит-тестах.
|
|
243
|
+
if ((process.env.NODE_ENV === 'test'))
|
|
244
|
+
devices[deviceId] = void 0;
|
|
245
|
+
return (
|
|
246
|
+
// Строится единожды на скрипт, далее берётся из кэша.
|
|
247
|
+
// При повторном вызове переданные свойства будут проигнорированы.
|
|
248
|
+
devices[deviceId] ??= createDevice(type, deviceId, options.props ?? {}, props ?? {}, options));
|
|
93
249
|
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Определяет структуру проводного устройства.
|
|
253
|
+
* @since 0.1.0
|
|
254
|
+
**/
|
|
255
|
+
function defineWiredDevice(options) {
|
|
256
|
+
return defineDevice('wired', options);
|
|
257
|
+
}
|
|
258
|
+
let virtualPerScript = 0;
|
|
259
|
+
/**
|
|
260
|
+
* Обеспечивает нумерацию безымянных виртуальных
|
|
261
|
+
* устройств в пределах скрипта.
|
|
262
|
+
*
|
|
263
|
+
**/
|
|
264
|
+
const getNextVirtualNumber = () => virtualPerScript += 1;
|
|
265
|
+
/**
|
|
266
|
+
* Использует {@link getNextVirtualNumber} и возвращает
|
|
267
|
+
* номер устройства, дополненный ведущими нулями.
|
|
268
|
+
*
|
|
269
|
+
* @returns Дополненная ведущими нулями строка с номером.
|
|
270
|
+
*
|
|
271
|
+
**/
|
|
272
|
+
function getNextVirtualNumberPadded() {
|
|
273
|
+
const nextNumber = String(getNextVirtualNumber());
|
|
274
|
+
if (nextNumber.length < 2)
|
|
275
|
+
return '00' + nextNumber;
|
|
276
|
+
if (nextNumber.length < 3)
|
|
277
|
+
return '0' + nextNumber;
|
|
278
|
+
return nextNumber;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Определяет структуру виртуального устройства.
|
|
282
|
+
* @since 0.1.0
|
|
283
|
+
**/
|
|
284
|
+
function defineVirtualDevice(options) {
|
|
285
|
+
// Виртуальные устройства определяют заголовок по умолчанию,
|
|
286
|
+
// если он не передан во входящих параметрах.
|
|
287
|
+
//
|
|
288
|
+
options.props = assign({}, {
|
|
289
|
+
title: {
|
|
290
|
+
type: Object, defaultValue: () => {
|
|
291
|
+
const match = /\/([^/]+?)(?:\.js)?$/.exec(__filename);
|
|
292
|
+
return `Virtual #${getNextVirtualNumberPadded()}${match?.[1] ? ` at '${match[1]}'` : ''}`;
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}, options.props);
|
|
296
|
+
return defineDevice('virtual', options);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Определяет структуру zigbee-устройства.
|
|
300
|
+
* @since 0.1.0
|
|
301
|
+
**/
|
|
302
|
+
function defineZigbeeDevice(options) {
|
|
303
|
+
return defineDevice('zigbee', options);
|
|
105
304
|
}
|
|
106
305
|
|
|
107
306
|
/**
|
|
@@ -121,4 +320,4 @@ function getDeviceSafe(deviceId, isReadyFunc = () => true) {
|
|
|
121
320
|
};
|
|
122
321
|
}
|
|
123
322
|
|
|
124
|
-
export { defineZigbeeDevice, getControlSafe, getDeviceSafe };
|
|
323
|
+
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
|
|
4
|
+
"version": "0.1.0",
|
|
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
|
|
40
|
-
"@mirta/globals": "0.0
|
|
41
|
-
"@mirta/polyfills": "0.0
|
|
39
|
+
"@mirta/basics": "0.1.0",
|
|
40
|
+
"@mirta/globals": "0.1.0",
|
|
41
|
+
"@mirta/polyfills": "0.1.0"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|