create-mirta 0.0.2 → 0.0.4

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.
Files changed (24) hide show
  1. package/dist/index.mjs +114 -9
  2. package/dist/locales/en-US.json +12 -2
  3. package/dist/locales/ru-RU.json +12 -2
  4. package/dist/templates/base/package.json +2 -2
  5. package/dist/templates/config/store/package.json +1 -1
  6. package/dist/templates/config/typescript/package.json +2 -2
  7. package/dist/templates/config/typescript/tsconfig.json +4 -3
  8. package/dist/templates/config/vitest/tsconfig.json +7 -0
  9. package/dist/templates/example/base/src/wb-rules/01-virtual-device.ts +37 -0
  10. package/dist/templates/example/base/src/wb-rules/02-control-monitoring.ts +36 -0
  11. package/dist/templates/example/base/src/wb-rules/03-master-switch.ts +55 -0
  12. package/dist/templates/example/base/src/wb-rules/04-pir-timeout.ts +37 -0
  13. package/dist/templates/example/base/src/wb-rules/05-same-rules.ts +58 -0
  14. package/dist/templates/example/base/src/wb-rules/06-timed-activation.ts +51 -0
  15. package/dist/templates/example/base/src/wb-rules/07-roller-shutters.ts +85 -0
  16. package/dist/templates/example/base/src/wb-rules/08-count-discrete.ts +57 -0
  17. package/dist/templates/example/base/src/wb-rules/09-impulse-counter.ts +46 -0
  18. package/dist/templates/example/base/src/wb-rules/10-invert-control-value.ts +33 -0
  19. package/dist/templates/example/base/src/wb-rules/11-press-counter.ts +112 -0
  20. package/dist/templates/example/base/src/wb-rules/12-msw-rule-co2.ts +47 -0
  21. package/dist/templates/example/base/src/wb-rules/13-msw-rule-co2-multi.ts +51 -0
  22. package/dist/templates/example/base/src/wb-rules/14-max-motion.ts +33 -0
  23. package/dist/templates/example/base/src/wb-rules/15-system-buzzer.ts +74 -0
  24. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -140,7 +140,7 @@ function usePrompts(messages) {
140
140
  cancel,
141
141
  prompt,
142
142
  step: p.log.step,
143
- note: p.note,
143
+ message: p.log.message,
144
144
  inlineSub,
145
145
  };
146
146
  }
@@ -174,6 +174,10 @@ ${yellow$1('Feature flags:')}
174
174
  ${dim$1('Add store for state management')}
175
175
 
176
176
  ${yellow$1('Options:')}
177
+ --ssh
178
+ ${dim$1('Set SSH destination of deployment process as [username@][hostname][:port]')}
179
+ --rutoken
180
+ ${dim$1('Use Rutoken ECP as encrypted store of SSH private key')}
177
181
  -v, --version
178
182
  ${dim$1('Display the version number of this CLI')}
179
183
  -f, --force
@@ -203,6 +207,10 @@ ${yellow$1('Флаги функционала:')}
203
207
  ${dim$1('Добавить хранилище состояний')}
204
208
 
205
209
  ${yellow$1('Опции:')}
210
+ --ssh
211
+ ${dim$1('Настроить подключение SSH для деплоя в формате [username@][hostname][:port]')}
212
+ --rutoken
213
+ ${dim$1('Использовать Рутокен ЭЦП в качестве хранилища для закрытого ключа SSH')}
206
214
  -v, --version
207
215
  ${dim$1('Отобразить номер версии данного CLI')}
208
216
  -f, --force
@@ -232,6 +240,12 @@ const featureFlags = ({
232
240
  });
233
241
  const allOptions = ({
234
242
  ...featureFlags,
243
+ ssh: {
244
+ type: 'string',
245
+ },
246
+ rutoken: {
247
+ type: 'boolean',
248
+ },
235
249
  version: {
236
250
  type: 'boolean',
237
251
  short: 'v',
@@ -240,16 +254,23 @@ const allOptions = ({
240
254
  type: 'boolean',
241
255
  short: 'f',
242
256
  },
243
- help: {
244
- type: 'boolean',
245
- short: 'h',
246
- },
247
257
  bare: {
248
258
  type: 'boolean',
249
259
  short: 'b',
250
260
  },
261
+ help: {
262
+ type: 'boolean',
263
+ short: 'h',
264
+ },
251
265
  });
252
266
 
267
+ const urlRegex = /(?:(?<username>.+?)@)?(?:(?<hostname>[^:@\s]+))?(?::(?<port>\d+))?/;
268
+ function parseUrl(value) {
269
+ if (!value)
270
+ return {};
271
+ return (urlRegex.exec(value))?.groups ?? {};
272
+ }
273
+
253
274
  const isObject = (val) => typeof val === 'object';
254
275
 
255
276
  function sortDependencies(packageJson) {
@@ -329,7 +350,7 @@ function renderTemplate(sourcePath, targetPath) {
329
350
  renderJson(sourcePath, targetPath, result => sortDependencies(result));
330
351
  return;
331
352
  }
332
- if (['extensions.json', 'settings.json', 'tasks.json'].includes(filename) && fs.existsSync(targetPath)) {
353
+ if (['extensions.json', 'settings.json', 'tasks.json', 'tsconfig.json'].includes(filename) && fs.existsSync(targetPath)) {
333
354
  renderJson(sourcePath, targetPath);
334
355
  return;
335
356
  }
@@ -500,7 +521,7 @@ const featureOptions = [
500
521
  hint: messages.addVitest.hint,
501
522
  },
502
523
  ];
503
- const { prompt, cancel, step, inlineSub } = usePrompts(messages);
524
+ const { prompt, cancel, step, message, inlineSub } = usePrompts(messages);
504
525
  function isValidPackageName(packageName) {
505
526
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
506
527
  .test(packageName);
@@ -536,17 +557,26 @@ async function run() {
536
557
  const hasPositionalDir = targetDir && targetDir !== '.';
537
558
  const defaultProjectName = hasPositionalDir ? targetDir : 'wb-rules-mirta';
538
559
  const shouldOverwrite = argv.force;
560
+ const sshAddress = parseUrl(argv.ssh);
561
+ const sshDefaultUser = 'root';
562
+ const sshDefaultHost = '10.200.200.1';
563
+ const sshDefaultPort = '22';
564
+ const sshFullyDefined = !!(sshAddress.username && sshAddress.hostname);
539
565
  const scope = {
540
566
  projectName: defaultProjectName,
541
567
  projectRoot: '',
542
568
  packageName: defaultProjectName,
543
569
  shouldOverwrite,
544
570
  features: [],
571
+ sshUsername: sshAddress.username,
572
+ sshHostname: sshAddress.hostname,
573
+ sshPort: sshAddress.port,
574
+ rutoken: argv.rutoken,
545
575
  };
546
576
  console.log(banner);
547
577
  console.log(messages.title);
548
578
  console.log();
549
- intro(chalk.bgYellow.black(` ${messages.intro} `));
579
+ intro(chalk.bgBlackBright.black(` ${messages.captions.intro} `));
550
580
  if (!targetDir) {
551
581
  const answer = await prompt(text({
552
582
  message: messages.projectName.message,
@@ -590,6 +620,58 @@ async function run() {
590
620
  process.exit(0);
591
621
  }
592
622
  }
623
+ message(chalk.bgBlackBright.black(` ${messages.captions.deploy} `));
624
+ if (!scope.sshUsername) {
625
+ scope.sshUsername = await prompt(text({
626
+ message: messages.ssh.username,
627
+ placeholder: sshDefaultUser,
628
+ defaultValue: sshDefaultUser,
629
+ validate: (value) => {
630
+ if (value.length !== 0 && value.trim().length === 0)
631
+ return messages.validation.required;
632
+ },
633
+ }));
634
+ }
635
+ else {
636
+ step(`${messages.ssh.username}\n${dim(scope.sshUsername)}`);
637
+ }
638
+ if (!scope.sshHostname) {
639
+ scope.sshHostname = await prompt(text({
640
+ message: messages.ssh.host,
641
+ placeholder: sshDefaultHost,
642
+ defaultValue: sshDefaultHost,
643
+ validate: (value) => {
644
+ if (value.length !== 0 && value.trim().length === 0)
645
+ return messages.validation.required;
646
+ },
647
+ }));
648
+ }
649
+ else {
650
+ step(`${messages.ssh.host}\n${dim(scope.sshHostname)}`);
651
+ }
652
+ if (!sshFullyDefined && !scope.sshPort) {
653
+ scope.sshPort = await prompt(text({
654
+ message: messages.ssh.port,
655
+ placeholder: sshDefaultPort,
656
+ defaultValue: sshDefaultPort,
657
+ validate: (value) => {
658
+ if (value.length !== 0 && value.trim().length === 0)
659
+ return messages.validation.required;
660
+ },
661
+ }));
662
+ }
663
+ else if (scope.sshPort) {
664
+ step(`${messages.ssh.port}\n${dim(scope.sshPort)}`);
665
+ }
666
+ if (!sshFullyDefined && !scope.rutoken) {
667
+ scope.rutoken = await prompt(confirm({
668
+ message: `${messages.ssh.useRutoken} ${dim(messages.accent.ifConfigured)}`,
669
+ initialValue: false,
670
+ }));
671
+ }
672
+ else if (scope.rutoken) {
673
+ step(`${messages.ssh.useRutoken} ${dim(messages.accent.ifConfigured)}\n${dim('Yes')}`);
674
+ }
593
675
  if (!isFeatureFlagsUsed) {
594
676
  scope.features = await prompt(multiselect({
595
677
  message: `${messages.featureSelection.message}${inlineSub(dim(messages.featureSelection.hint))}`,
@@ -610,7 +692,30 @@ async function run() {
610
692
  fs.mkdirSync(root);
611
693
  }
612
694
  step(`${messages.status.scaffolding} ${yellow(root)}`);
613
- const pkg = { name: scope.packageName, version: '0.0.0' };
695
+ const pkg = {
696
+ name: scope.packageName,
697
+ version: '0.0.0',
698
+ scripts: {
699
+ 'wb:deploy': '',
700
+ },
701
+ };
702
+ const deployScript = ['rsync'];
703
+ if (process.platform === 'win32') {
704
+ // Run rsync trough WSL on Windows systems.
705
+ deployScript.unshift('wsl ');
706
+ }
707
+ if (scope.rutoken || scope.sshPort) {
708
+ deployScript.push(' -e \'ssh');
709
+ if (scope.rutoken)
710
+ deployScript.push(' -I /usr/lib/librtpkcs11ecp.so');
711
+ if (scope.sshPort)
712
+ deployScript.push(` -p ${scope.sshPort}`);
713
+ deployScript.push('\'');
714
+ }
715
+ deployScript.push(' -rltzvgO --progress --delete --exclude=\'alarms.conf\'');
716
+ deployScript.push(' --groupmap=\'*:developers\' dist/es5/*');
717
+ deployScript.push(` '${scope.sshUsername}@${scope.sshHostname}:/mnt/data/etc/'`);
718
+ pkg.scripts['wb:deploy'] = deployScript.join('');
614
719
  fs.writeFileSync(resolve(root, 'package.json'), JSON.stringify(pkg, null, 2));
615
720
  const templateRoot = fileURLToPath(new URL(templatesPath, import.meta.url));
616
721
  const render = function (templateName) {
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "title": "The Framework to write wb-rules in TypeScript",
3
- "intro": "Project Settings",
3
+ "captions": {
4
+ "intro": "Project Settings",
5
+ "deploy": "Deploy Settings"
6
+ },
4
7
  "projectName": {
5
8
  "message": "Project Name (target directory):"
6
9
  },
@@ -16,6 +19,12 @@
16
19
  "message": "is not empty.",
17
20
  "confirmDelete": "Remove existing files before continue?"
18
21
  },
22
+ "ssh": {
23
+ "username": "🔑 SSH Username:",
24
+ "host": "🌐 SSH Host:",
25
+ "port": "🚩 SSH Port:",
26
+ "useRutoken": "Use Rutoken ECP"
27
+ },
19
28
  "featureSelection": {
20
29
  "message": "Select features to include in your project:",
21
30
  "hint": "↑/↓ to navigate, space to select, A to toggle all, Enter to confirm"
@@ -44,7 +53,8 @@
44
53
  "installingDependencies": "Calling the package manager"
45
54
  },
46
55
  "accent": {
47
- "recommended": "(recommended)"
56
+ "recommended": "(recommended)",
57
+ "ifConfigured": "(if configured)"
48
58
  },
49
59
  "dependencies": {
50
60
  "question": "Install project dependencies?",
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "title": "Фреймворк для создания правил wb-rules на TypeScript",
3
- "intro": "Параметры проекта",
3
+ "captions": {
4
+ "intro": "Параметры проекта",
5
+ "deploy": "Параметры деплоя на контроллер"
6
+ },
4
7
  "projectName": {
5
8
  "message": "Название (целевая директория):",
6
9
  "errorMessage": "Требуется указать значение"
@@ -17,6 +20,12 @@
17
20
  "message": "содержит файлы.",
18
21
  "confirmDelete": "Удалить их?"
19
22
  },
23
+ "ssh": {
24
+ "username": "🔑 Пользователь SSH:",
25
+ "host": "🌐 Хост SSH:",
26
+ "port": "🚩 Порт SSH:",
27
+ "useRutoken": "Использовать Рутокен ЭЦП?"
28
+ },
20
29
  "featureSelection": {
21
30
  "message": "Выберите добавляемый функционал:",
22
31
  "hint": "↑/↓ для навигации, пробел для выбора, A - выбрать всё, Enter - подтвердить"
@@ -45,7 +54,8 @@
45
54
  "installingDependencies": "Вызов менеджера пакетов"
46
55
  },
47
56
  "accent": {
48
- "recommended": "(рекомендуется)"
57
+ "recommended": "(рекомендуется)",
58
+ "ifConfigured": "(если настроен)"
49
59
  },
50
60
  "dependencies": {
51
61
  "question": "Установить требуемые зависимости?",
@@ -8,10 +8,10 @@
8
8
  "build:dev": "cross-env NODE_ENV=development rollup -c"
9
9
  },
10
10
  "dependencies": {
11
- "mirta": "0.0.2"
11
+ "mirta": "0.0.4"
12
12
  },
13
13
  "devDependencies": {
14
- "@mirta/rollup": "0.0.2",
14
+ "@mirta/rollup": "0.0.4",
15
15
  "cross-env": "^7.0.3",
16
16
  "rollup": "^4.45.1"
17
17
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "dependencies": {
3
- "@mirta/store": "0.0.2"
3
+ "@mirta/store": "0.0.4"
4
4
  }
5
5
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "devDependencies": {
3
- "@mirta/globals": "0.0.2",
4
- "@mirta/tsconfig": "0.0.2",
3
+ "@mirta/globals": "0.0.4",
4
+ "@mirta/tsconfig": "0.0.4",
5
5
  "@types/node": "^24.1.0",
6
6
  "typescript": "^5.8.3"
7
7
  }
@@ -2,12 +2,13 @@
2
2
  "extends": "@mirta/tsconfig",
3
3
  "compilerOptions": {
4
4
  "paths": {
5
- "@wbm/*": ["./src/wb-rules-modules/*"]
5
+ "@wbm/*": [
6
+ "./src/wb-rules-modules/*"
7
+ ]
6
8
  },
7
9
  "types": [
8
10
  "@mirta/globals",
9
- "node",
10
- "vitest/globals"
11
+ "node"
11
12
  ]
12
13
  }
13
14
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": [
4
+ "vitest/globals"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,37 @@
1
+ // Виртуальное устройство
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ const deviceName = 'my-virtual-device'
7
+
8
+ defineVirtualDevice(deviceName, {
9
+ title: { 'en': 'My Virtual Device', 'ru': 'Мое виртуальное устройство' },
10
+ cells: {
11
+ value: {
12
+ title: { 'en': 'Value', 'ru': 'Значение' },
13
+ type: 'range',
14
+ value: 1,
15
+ min: 1,
16
+ max: 3,
17
+ },
18
+ state: {
19
+ title: { 'en': 'State', 'ru': 'Состояние' },
20
+ type: 'value',
21
+ value: 1,
22
+ enum: {
23
+ 1: { 'en': 'Normal', 'ru': 'В норме' },
24
+ 2: { 'en': 'Warning', 'ru': 'Внимание' },
25
+ 3: { 'en': 'Crash', 'ru': 'Авария' } },
26
+ },
27
+ },
28
+ })
29
+
30
+ defineRule({
31
+ whenChanged: deviceName + '/value',
32
+ then: function (newValue) {
33
+
34
+ dev[`${deviceName}/state`] = newValue
35
+
36
+ },
37
+ })
@@ -0,0 +1,36 @@
1
+ // Слежение за контролом
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ defineRule({
7
+ whenChanged: 'wb-gpio/D1_IN',
8
+ then: function (newValue) {
9
+
10
+ dev['wb-gpio/Relay_2'] = newValue
11
+ dev['wb-mrm2_6/Relay 1'] = newValue
12
+
13
+ },
14
+ })
15
+
16
+ // То же самое, но с виртуальным девайсом в качестве источника событий.
17
+
18
+ defineVirtualDevice('simple_test', {
19
+ title: 'Simple Switch',
20
+ cells: {
21
+ enabled: {
22
+ type: 'switch',
23
+ value: false,
24
+ },
25
+ },
26
+ })
27
+
28
+ defineRule('simple_switch', {
29
+ whenChanged: 'simple_test/enabled',
30
+ then: function (newValue) {
31
+
32
+ dev['wb-gpio/Relay_2'] = newValue
33
+ dev['wb-mrm2_6/Relay 1'] = newValue
34
+
35
+ },
36
+ })
@@ -0,0 +1,55 @@
1
+ // Мастер-выключатель с восстановлением последнего состояния
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ defineVirtualDevice('power_off', {
7
+ title: 'Мастер-выключатель',
8
+ cells: {
9
+ power_off: {
10
+ type: 'pushbutton',
11
+ },
12
+ },
13
+ })
14
+
15
+ const ps = new PersistentStorage('power-storage', { global: true })
16
+
17
+ // Для обеспечения возможности распознавания обращения
18
+ // к контролу в dev, после массива добавляется 'as const'.
19
+
20
+ const lights = [
21
+ 'wb-mdm3_50/K1',
22
+ 'wb-mdm3_50/K2',
23
+ 'wb-mdm3_50/K3',
24
+ ] as const
25
+
26
+ let isPowerOff = true
27
+
28
+ defineRule({
29
+ whenChanged: ['wb-gpio/A1_IN', 'power_off/power_off'],
30
+ then: function () {
31
+
32
+ if (isPowerOff) {
33
+
34
+ lights.forEach(function (light) {
35
+
36
+ ps[light] = dev[light]
37
+ dev[light] = false
38
+
39
+ })
40
+
41
+ }
42
+ else {
43
+
44
+ lights.forEach(function (light) {
45
+
46
+ dev[light] = ps[light] as WbRules.MqttValue
47
+
48
+ })
49
+
50
+ }
51
+
52
+ isPowerOff = !isPowerOff
53
+
54
+ },
55
+ })
@@ -0,0 +1,37 @@
1
+ // Детектор движения c таймаутом
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ const motion_timer_1_timeout_ms = 30 * 1000
7
+ let motion_timer_1_id: NodeJS.Timeout | undefined
8
+
9
+ defineRule('motion_detector_1', {
10
+ whenChanged: 'wb-gpio/D2_IN',
11
+ then: function (newValue) {
12
+
13
+ // Обнаружено движение
14
+ if (newValue) {
15
+
16
+ // Включаем свет
17
+ dev['wb-gpio/Relay_1'] = true
18
+
19
+ if (motion_timer_1_id)
20
+ clearTimeout(motion_timer_1_id)
21
+
22
+ motion_timer_1_id = setTimeout(function () {
23
+
24
+ // Выключаем свет
25
+ dev['wb-gpio/Relay_1'] = false
26
+
27
+ // Очистка идентификатора таймера.
28
+ // (прим. void 0 - то же самое, что и undefined).
29
+ //
30
+ motion_timer_1_id = void 0
31
+
32
+ }, motion_timer_1_timeout_ms)
33
+
34
+ }
35
+
36
+ },
37
+ })
@@ -0,0 +1,58 @@
1
+ // Создание однотипных правил
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+ //
6
+ // Если таких детекторов движения нужно несколько,
7
+ // то чтобы не копировать код, можно обернуть создание
8
+ // правила и переменных в функцию:
9
+
10
+ /**
11
+ * Создаёт правило реакции на сработку датчика движения.
12
+ * Какой датчик и какое реле, станет известно при вызове функции.
13
+ *
14
+ * @param name Наименование правила.
15
+ * @param timeout_ms Задержка срабатывания, в мс.
16
+ * @param detector_control Идентификатор датчика.
17
+ * @param relay_control Идентификатор реле.
18
+ */
19
+ function makeMotionDetector(
20
+ name: string,
21
+ timeout_ms: number,
22
+ detector_control: string,
23
+ relay_control: string
24
+ ) {
25
+
26
+ let motion_timer_id: NodeJS.Timeout | undefined
27
+
28
+ defineRule(name, {
29
+ whenChanged: `wb-gpio/${detector_control}`,
30
+ then: function (newValue) {
31
+
32
+ if (!newValue) {
33
+
34
+ dev[`wb-gpio/${relay_control}`] = true
35
+
36
+ if (motion_timer_id) {
37
+
38
+ clearTimeout(motion_timer_id)
39
+
40
+ }
41
+
42
+ motion_timer_id = setTimeout(function () {
43
+
44
+ dev['wb-gpio/relay_control'] = false
45
+ motion_timer_id = void 0
46
+
47
+ }, timeout_ms)
48
+
49
+ }
50
+
51
+ },
52
+ })
53
+
54
+ }
55
+
56
+ makeMotionDetector('motion_detector_1', 20000, 'EXT1_DR1', 'EXT2_R3A1')
57
+ makeMotionDetector('motion_detector_2', 10000, 'EXT1_DR2', 'EXT2_R3A2')
58
+ makeMotionDetector('motion_detector_3', 10000, 'EXT1_DR3', 'EXT2_R3A3')
@@ -0,0 +1,51 @@
1
+ // Активация правила только в определённое время
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ const motion_timer_1_timeout_ms = 5 * 1000
7
+ let motion_timer_1_id: NodeJS.Timeout | undefined
8
+
9
+ defineRule('motion_detector_1', {
10
+ whenChanged: 'wb-gpio/A1_IN',
11
+ then: function (newValue) {
12
+
13
+ const date = new Date()
14
+
15
+ // time point marking the beginning of the interval
16
+ // i.e. "today, at HH:MM". All dates are in UTC!
17
+ const date_start = new Date(date)
18
+ date_start.setHours(9)
19
+ date_start.setMinutes(30)
20
+
21
+ // time point marking the end of the interval
22
+ const date_end = new Date(date)
23
+ date_end.setHours(17)
24
+ date_end.setMinutes(10)
25
+
26
+ // if time is between 09:30 and 17:10 UTC
27
+ if ((date > date_start) && (date < date_end)) {
28
+
29
+ if (newValue) {
30
+
31
+ dev['wb-gpio/EXT1_R3A1'] = 1
32
+
33
+ if (motion_timer_1_id) {
34
+
35
+ clearTimeout(motion_timer_1_id)
36
+
37
+ }
38
+
39
+ motion_timer_1_id = setTimeout(function () {
40
+
41
+ dev['wb-gpio/EXT1_R3A1'] = 0
42
+ motion_timer_1_id = void 0
43
+
44
+ }, motion_timer_1_timeout_ms)
45
+
46
+ }
47
+
48
+ }
49
+
50
+ },
51
+ })
@@ -0,0 +1,85 @@
1
+ // Роллеты
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ const suffix = '1' // must be different in different JS files
7
+
8
+ const relay_up = 'lc103_4/Relay 1'
9
+ const relay_down = 'lc103_4/Relay 2'
10
+
11
+ const timeout_s = 15
12
+
13
+ // End of settings
14
+
15
+ let relay_up_timer_id: NodeJS.Timeout | undefined
16
+ let relay_down_timer_id: NodeJS.Timeout | undefined
17
+
18
+ defineRule('roller_shutter_up_on' + suffix, {
19
+ asSoonAs: function () {
20
+
21
+ return dev[relay_up] as boolean
22
+
23
+ },
24
+ then: function () {
25
+
26
+ if (relay_up_timer_id) {
27
+
28
+ clearTimeout(relay_up_timer_id)
29
+
30
+ };
31
+
32
+ relay_up_timer_id = setTimeout(function () {
33
+
34
+ return dev[relay_up] = 0
35
+
36
+ }, timeout_s * 1000)
37
+
38
+ },
39
+ })
40
+
41
+ defineRule('roller_shutter_down_on' + suffix, {
42
+ asSoonAs: function () {
43
+
44
+ return dev[relay_down] as boolean
45
+
46
+ },
47
+ then: function () {
48
+
49
+ if (relay_down_timer_id) {
50
+
51
+ clearTimeout(relay_down_timer_id)
52
+
53
+ };
54
+
55
+ relay_down_timer_id = setTimeout(function () {
56
+
57
+ dev[relay_down] = 0
58
+
59
+ }, timeout_s * 1000)
60
+
61
+ },
62
+ })
63
+
64
+ defineRule('roller_shutter_both_on' + suffix, {
65
+ asSoonAs: function () {
66
+
67
+ return dev[relay_up] as boolean
68
+ && dev[relay_down] as boolean
69
+
70
+ },
71
+ then: function () {
72
+
73
+ if (relay_up_timer_id)
74
+ clearTimeout(relay_up_timer_id)
75
+
76
+ if (relay_down_timer_id)
77
+ clearTimeout(relay_down_timer_id)
78
+
79
+ dev[relay_up] = 0
80
+ dev[relay_down] = 0
81
+
82
+ log('Both roller shutter relays on, switching them off')
83
+
84
+ },
85
+ })
@@ -0,0 +1,57 @@
1
+ // Подсчет импульсов на дискретном входе
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ import { isNumber } from 'mirta'
7
+
8
+ // --- Виртуальное устройство для отображения количества импульсов с кнопкой сброса ---
9
+ defineVirtualDevice('pulse_counter', {
10
+ title: 'Pulse counter',
11
+ cells: {
12
+ impulses: {
13
+ type: 'value',
14
+ value: 0,
15
+ },
16
+ reset: {
17
+ type: 'pushbutton',
18
+ },
19
+ },
20
+ })
21
+
22
+ // --- Логика подсчета импульсов ---
23
+ let impulseCount = 0 // Счетчик импульсов
24
+ let lastInputState = 0 // Последнее состояние входа
25
+ const inputChannel = 'wb-gpio/MOD1_IN2' // Топик дискретного входа
26
+
27
+ // Обработка импульсов: с 0 на 1
28
+ defineRule('count_impulses', {
29
+ whenChanged: inputChannel, // Правило срабатывает при любом изменении на дискретном входе, с 0 на 1 и наоборот
30
+ then: function (newValue) {
31
+
32
+ // Сужение диапазона типов до number.
33
+ if (!isNumber(newValue))
34
+ return
35
+
36
+ if (lastInputState == 0 && newValue == 1) { // Проверка фронта импульса с 0 на 1
37
+
38
+ impulseCount += 1 // Счетчик импульсов
39
+ dev.pulse_counter.impulses = impulseCount // Отображение импульсов в виртуальном устройстве
40
+
41
+ }
42
+
43
+ lastInputState = newValue // Сохранение значения фронта импульса, используется в условии выше
44
+
45
+ },
46
+ })
47
+
48
+ // Обработка кнопки сброса
49
+ defineRule('reset_counter', {
50
+ whenChanged: 'pulse_counter/reset',
51
+ then: function () {
52
+
53
+ impulseCount = 0 // Сброс счетчика
54
+ dev.pulse_counter.impulses = impulseCount
55
+
56
+ },
57
+ })
@@ -0,0 +1,46 @@
1
+ // Импульсные счетчики
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ import { isNumber, isString } from 'mirta'
7
+
8
+ const meterCorrection = 123120 // Корректировочное значение счетчика в литрах
9
+ const counterCorrection = 7 // Корректировочное значение WB-MCM8 в импульсах
10
+ const inpulseValue = 10 // Количество литров на один импульс
11
+
12
+ defineVirtualDevice('water_meters', { // Создаем виртуальный девайс для отображения в веб интерфейсе.
13
+ title: 'Счетчики воды',
14
+ cells: {
15
+ water_meter_1: {
16
+ type: 'value',
17
+ value: 0,
18
+ },
19
+ },
20
+ })
21
+
22
+ defineRule('water_meter_1', {
23
+ whenChanged: 'wb-mcm8_29/Input 1 counter',
24
+ then: function (newValue) {
25
+
26
+ if (newValue) {
27
+
28
+ // Здесь было не совсем очевидно из оригинального примера,
29
+ // что приходит в качестве newValue.
30
+ //
31
+ // Поэтому перестраховка с проверкой типов - распознаёт как строки, так и числа.
32
+
33
+ const intValue = isString(newValue)
34
+ ? parseInt(newValue)
35
+ : (
36
+ isNumber(newValue)
37
+ ? newValue
38
+ : 0
39
+ )
40
+
41
+ dev['water_meters/water_meter_1'] = ((intValue - counterCorrection) * inpulseValue) + meterCorrection // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение.
42
+
43
+ }
44
+
45
+ },
46
+ })
@@ -0,0 +1,33 @@
1
+ // Инвертирование значения контрола
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ defineVirtualDevice('my-invert-buzzer', {
7
+ title: 'Buzzer Invert',
8
+ cells: {
9
+ Disabled: {
10
+ title: 'disabled',
11
+ type: 'switch',
12
+ value: !dev['buzzer/enabled'],
13
+ },
14
+ },
15
+ })
16
+
17
+ defineRule({
18
+ whenChanged: ['buzzer/enabled'],
19
+ then: function (newValue) {
20
+
21
+ dev['my-invert-buzzer/Disabled'] = !newValue
22
+
23
+ },
24
+ })
25
+
26
+ defineRule({
27
+ whenChanged: ['my-invert-buzzer/Disabled'],
28
+ then: function (newValue) {
29
+
30
+ dev['buzzer/enabled'] = !newValue
31
+
32
+ },
33
+ })
@@ -0,0 +1,112 @@
1
+ // Обработка счётчиков нажатий
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ /* ---------------------------------- */
7
+ /* 1. Single Press Counter: On action */
8
+ /* ---------------------------------- */
9
+
10
+ defineRule({
11
+ whenChanged: 'wb-mcm8_20/Input 1 Single Press Counter',
12
+ then: function () {
13
+
14
+ dev['wb-mdm3_58/K1'] = true
15
+
16
+ },
17
+ })
18
+
19
+ /* ----------------------------------- */
20
+ /* 2. Double Press Counter: Off action */
21
+ /* ----------------------------------- */
22
+
23
+ defineRule({
24
+ whenChanged: 'wb-mcm8_20/Input 1 Double Press Counter',
25
+ then: function () {
26
+
27
+ dev['wb-mdm3_58/K1'] = false
28
+
29
+ },
30
+ })
31
+
32
+ /* ------------------------------------------ */
33
+ /* 3. Long Press Counter: Increase brightness */
34
+ /* ------------------------------------------ */
35
+
36
+ defineRule({
37
+ whenChanged: 'wb-mcm8_20/Input 1 Long Press Counter',
38
+ then: function () {
39
+
40
+ // Start a timer that will increase the value of the control
41
+ startTicker('input1_long_press', 75)
42
+
43
+ },
44
+ })
45
+
46
+ // A rule that will increase the brightness on a timer
47
+ defineRule({
48
+ when: function () {
49
+
50
+ return timers.input1_long_press.firing
51
+
52
+ },
53
+ then: function () {
54
+
55
+ let i = dev['wb-mdm3_58/Channel 1'] as number
56
+
57
+ if (i < 100 && dev['wb-mcm8_20/Input 1']) {
58
+
59
+ i += 1
60
+
61
+ dev['wb-mdm3_58/Channel 1'] = i
62
+
63
+ }
64
+ else {
65
+
66
+ timers.input1_long_press.stop()
67
+
68
+ }
69
+
70
+ },
71
+ })
72
+
73
+ /* ------------===-------------------------------- */
74
+ /* 4. Shortlong Press Counter: Decrease brightness */
75
+ /* ---------------===----------------------------- */
76
+
77
+ defineRule({
78
+ whenChanged: 'wb-mcm8_20/Input 1 Shortlong Press Counter',
79
+ then: function () {
80
+
81
+ // Start a timer that will decrease the value of the control
82
+ startTicker('input1_shortlong_press', 75)
83
+
84
+ },
85
+ })
86
+
87
+ // A rule that will decrease the brightness on a timer
88
+ defineRule({
89
+ when: function () {
90
+
91
+ return timers.input1_shortlong_press.firing
92
+
93
+ },
94
+ then: function () {
95
+
96
+ let i = dev['wb-mdm3_58/Channel 1'] as number
97
+
98
+ if (i > 0 && dev['wb-mcm8_20/Input 1']) {
99
+
100
+ i -= 1
101
+
102
+ dev['wb-mdm3_58/Channel 1'] = i
103
+
104
+ }
105
+ else {
106
+
107
+ timers.input1_shortlong_press.stop()
108
+
109
+ }
110
+
111
+ },
112
+ })
@@ -0,0 +1,47 @@
1
+ // Датчик MSW v.3, CO2
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ import { isNumber, isString } from 'mirta'
7
+
8
+ defineRule('msw3_co2', {
9
+ whenChanged: 'wb-msw-v3_97/CO2',
10
+ then: function (newValue, devName) {
11
+
12
+ // Предохранитель типов, чтобы TypeScript не ворчал почём зря.
13
+ // TODO: Переосмыслить типизацию 'then'
14
+ //
15
+ if (!isNumber(newValue) || !isString(devName))
16
+ return
17
+
18
+ const co2_good = newValue < 650
19
+ const co2_middle = newValue < 1000 && newValue > 651
20
+ const co2_bad = newValue > 1001
21
+
22
+ if (co2_good) {
23
+
24
+ dev[devName]['/Green LED'] = true
25
+ dev[devName]['/Red LED'] = false
26
+ dev[devName]['/LED Period (s)'] = 10
27
+
28
+ }
29
+
30
+ if (co2_middle) {
31
+
32
+ dev[devName]['/Green LED'] = true
33
+ dev[devName]['/Red LED'] = true
34
+ dev[devName]['/LED Period (s)'] = 5
35
+
36
+ }
37
+
38
+ if (co2_bad) {
39
+
40
+ dev[devName]['/Green LED'] = false
41
+ dev[devName]['/Red LED'] = true
42
+ dev[devName]['/LED Period (s)'] = 1
43
+
44
+ }
45
+
46
+ },
47
+ })
@@ -0,0 +1,51 @@
1
+ // Датчик MSW v.3, CO2 - несколько датчиков сразу
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ import { isNumber } from 'mirta'
7
+
8
+ function ruleCO2(devCO2: string, minCO2: number, maxCO2: number) {
9
+
10
+ log.debug('rule create', devCO2)
11
+ defineRule ('ruleCO2' + devCO2, {
12
+ whenChanged: devCO2 + '/CO2',
13
+ then: function (newValue) {
14
+
15
+ if (!isNumber(newValue))
16
+ return
17
+
18
+ log.info('ruleCO2 ' + devCO2 + ' enter with', newValue)
19
+ if (newValue < minCO2) {
20
+
21
+ dev[devCO2]['/LED Glow Duration (ms)'] = 50
22
+ dev[devCO2]['/Green LED'] = true
23
+ dev[devCO2]['/Red LED'] = false
24
+ dev[devCO2]['/LED Period (s)'] = 3
25
+
26
+ }
27
+ if ((newValue > minCO2) && (newValue < maxCO2)) {
28
+
29
+ dev[devCO2]['/LED Glow Duration (ms)'] = 50
30
+ dev[devCO2]['/Green LED'] = true
31
+ dev[devCO2]['/Red LED'] = true
32
+ dev[devCO2]['/LED Period (s)'] = 2
33
+
34
+ }
35
+ if (newValue > maxCO2) {
36
+
37
+ dev[devCO2]['/LED Glow Duration (ms)'] = 50
38
+ dev[devCO2]['/Green LED'] = false
39
+ dev[devCO2]['/Red LED'] = true
40
+ dev[devCO2]['/LED Period (s)'] = 1
41
+
42
+ }
43
+
44
+ },
45
+ })
46
+
47
+ }
48
+
49
+ ruleCO2('wb-msw-v3_97', 650, 1000)
50
+ ruleCO2('wb-msw-v3_98', 650, 1000)
51
+ ruleCO2('wb-msw-v3_11', 500, 700)
@@ -0,0 +1,33 @@
1
+ // Датчик MSW v.3, Max Motion
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ import { isNumber } from 'mirta'
7
+
8
+ defineRule('msw3_Motion', {
9
+ whenChanged: 'wb-msw-v3_97/Max Motion',
10
+ then: function (newValue) {
11
+
12
+ if (!isNumber(newValue))
13
+ return
14
+
15
+ if (newValue > 50) {
16
+
17
+ // TODO: Переосмыслить типизацию. Кому понравится постоянно дописывать 'as ...'?
18
+ //
19
+ if ((dev['wb-msw-v3_97/Illuminance'] as number) < 50) {
20
+
21
+ dev['wb-mr3_11/K1'] = true
22
+
23
+ }
24
+
25
+ }
26
+ else {
27
+
28
+ dev['wb-mr3_11/K1'] = false
29
+
30
+ }
31
+
32
+ },
33
+ })
@@ -0,0 +1,74 @@
1
+ // Системные правила, пищалка
2
+ //
3
+ // Адаптированная версия примера
4
+ // https://wirenboard.com/wiki/index.php?title=Rule_Examples
5
+
6
+ defineVirtualDevice('buzzer', {
7
+ title: 'Buzzer', //
8
+
9
+ cells: {
10
+ frequency: {
11
+ type: 'range',
12
+ value: 3000,
13
+ max: 7000,
14
+ },
15
+ volume: {
16
+ type: 'range',
17
+ value: 10,
18
+ max: 100,
19
+ },
20
+ enabled: {
21
+ type: 'switch',
22
+ value: false,
23
+ },
24
+ },
25
+ })
26
+
27
+ // setup pwm2
28
+ runShellCommand('echo 2 > /sys/class/pwm/pwmchip0/export')
29
+
30
+ function _buzzer_set_params() {
31
+
32
+ const period = 1.0 / (dev.buzzer.frequency as number) * 1E9
33
+ const duty_cycle = (dev.buzzer.volume as number) * 1.0 / 100 * period * 0.5
34
+
35
+ runShellCommand(`echo ${period} > /sys/class/pwm/pwmchip0/pwm2/period`)
36
+ runShellCommand(`echo ${duty_cycle} > /sys/class/pwm/pwmchip0/pwm2/duty_cycle`)
37
+
38
+ };
39
+
40
+ defineRule('_system_buzzer_params', {
41
+ whenChanged: [
42
+ 'buzzer/frequency',
43
+ 'buzzer/volume',
44
+ ],
45
+
46
+ then: function () {
47
+
48
+ if (dev.buzzer.enabled) {
49
+
50
+ _buzzer_set_params()
51
+
52
+ }
53
+
54
+ },
55
+ })
56
+
57
+ defineRule('_system_buzzer_onof', {
58
+ whenChanged: 'buzzer/enabled',
59
+ then: function () {
60
+
61
+ if (dev.buzzer.enabled) {
62
+
63
+ _buzzer_set_params()
64
+ runShellCommand('echo 1 > /sys/class/pwm/pwmchip0/pwm2/enable')
65
+
66
+ }
67
+ else {
68
+
69
+ runShellCommand('echo 0 > /sys/class/pwm/pwmchip0/pwm2/enable')
70
+
71
+ }
72
+
73
+ },
74
+ })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-mirta",
3
3
  "description": "🛠️ The recommended way to start a Mirta project.",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "license": "Unlicense",
6
6
  "keywords": [
7
7
  "mirta",