homematic-manager 3.0.0-beta.2 → 3.0.0-beta.21

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 (332) hide show
  1. package/README.md +2 -1
  2. package/data/easymode-time-selectors.json +285 -0
  3. package/data/icons/106_hm-lc-sw1-dr.webp +0 -0
  4. package/data/icons/108_hm-rc-dis-h-x-eu.webp +0 -0
  5. package/data/icons/109_hm-lc-sw1-pl-ct.webp +0 -0
  6. package/data/icons/110_hm-es-pmsw1-dr.webp +0 -0
  7. package/data/icons/112_hmip-wrc2.webp +0 -0
  8. package/data/icons/113_hmip-psm-ch.webp +0 -0
  9. package/data/icons/113_hmip-psm-it.webp +0 -0
  10. package/data/icons/113_hmip-psm-pe.webp +0 -0
  11. package/data/icons/113_hmip-psm-uk.webp +0 -0
  12. package/data/icons/113_hmip-psm.webp +0 -0
  13. package/data/icons/114_hm-lc-dim1t-fm-lf_3.webp +0 -0
  14. package/data/icons/116_hm-rc-4-3_brc-h3.webp +0 -0
  15. package/data/icons/118_hmip-swdo.webp +0 -0
  16. package/data/icons/119_hmip-rc8.webp +0 -0
  17. package/data/icons/120_hmip-etrv.webp +0 -0
  18. package/data/icons/121_hmip-wth.webp +0 -0
  19. package/data/icons/123_oligo.smart.ip.hm.webp +0 -0
  20. package/data/icons/125_hmip-smi.webp +0 -0
  21. package/data/icons/130_hmip-srh.webp +0 -0
  22. package/data/icons/131_hmip-wrc6.webp +0 -0
  23. package/data/icons/132_hmip-smo.webp +0 -0
  24. package/data/icons/133_hmip-asir.webp +0 -0
  25. package/data/icons/134_hmip-fsm.webp +0 -0
  26. package/data/icons/135_hmip-fsm16.webp +0 -0
  27. package/data/icons/136_hmip-miob.webp +0 -0
  28. package/data/icons/137_hmip-fal-c6.webp +0 -0
  29. package/data/icons/138_hmip-fal-c10.webp +0 -0
  30. package/data/icons/140_alpha-ip-rgb.webp +0 -0
  31. package/data/icons/141_alpha-ip-rgba.webp +0 -0
  32. package/data/icons/144_hmip-wgc.webp +0 -0
  33. package/data/icons/145_hmip-froll_hmip-fbl.webp +0 -0
  34. package/data/icons/146_hmip-sth.webp +0 -0
  35. package/data/icons/147_hmip-sthd.webp +0 -0
  36. package/data/icons/148_hmip-stho.webp +0 -0
  37. package/data/icons/149_hmip-sam.webp +0 -0
  38. package/data/icons/151_hmip-pcbs-bat.webp +0 -0
  39. package/data/icons/152_hmip-swdo-i.webp +0 -0
  40. package/data/icons/153_hmip-spi.webp +0 -0
  41. package/data/icons/154_hmip-spdr.webp +0 -0
  42. package/data/icons/155_hmip-whs2.webp +0 -0
  43. package/data/icons/156_hmip-mod-oc8.webp +0 -0
  44. package/data/icons/158_hmip-etrv-uk.webp +0 -0
  45. package/data/icons/159_hmip-mod-rc8.webp +0 -0
  46. package/data/icons/160_hmipw-drs4.webp +0 -0
  47. package/data/icons/161_hmipw-drs8.webp +0 -0
  48. package/data/icons/162_hmipw-drap.webp +0 -0
  49. package/data/icons/163_hmipw-drbl4.webp +0 -0
  50. package/data/icons/164_hmipw-dri16.webp +0 -0
  51. package/data/icons/165_hmipw-fio6.webp +0 -0
  52. package/data/icons/166_hmipw-drd3.webp +0 -0
  53. package/data/icons/167_hmipw-dri32.webp +0 -0
  54. package/data/icons/168_hmip-smi55.webp +0 -0
  55. package/data/icons/169_hmip-swo-pr.webp +0 -0
  56. package/data/icons/170_hmip-swo-pl.webp +0 -0
  57. package/data/icons/171_hmip-swo-b.webp +0 -0
  58. package/data/icons/172_hmip-swd.webp +0 -0
  59. package/data/icons/173_hmip-bsl.webp +0 -0
  60. package/data/icons/174_hmip-slo.webp +0 -0
  61. package/data/icons/175_hmip-group-heating.webp +0 -0
  62. package/data/icons/177_hmip-dbb.webp +0 -0
  63. package/data/icons/178_hmip-etrv-b1.webp +0 -0
  64. package/data/icons/179_hmip-swdm-b2.webp +0 -0
  65. package/data/icons/180_hmip-etrv-b.webp +0 -0
  66. package/data/icons/181_hmip-swdm.webp +0 -0
  67. package/data/icons/182_hmip-fci1.webp +0 -0
  68. package/data/icons/183_hmip-mod-tm.webp +0 -0
  69. package/data/icons/184_hmip-pcbs2.webp +0 -0
  70. package/data/icons/185_hmip-FCI6.webp +0 -0
  71. package/data/icons/186_hmip-mp3p.webp +0 -0
  72. package/data/icons/187_hmip-rcb1.webp +0 -0
  73. package/data/icons/188_hmip-etrv-c.webp +0 -0
  74. package/data/icons/190_hmip-sci.webp +0 -0
  75. package/data/icons/191_hmip-mod-ho.webp +0 -0
  76. package/data/icons/192_hmip-asir-o.webp +0 -0
  77. package/data/icons/193_hmip-wt.webp +0 -0
  78. package/data/icons/194_hmip-wrcd.webp +0 -0
  79. package/data/icons/195_hmipw-smi55.webp +0 -0
  80. package/data/icons/196_hmip-ASIR-2.webp +0 -0
  81. package/data/icons/197_hmip-swdo-pl.webp +0 -0
  82. package/data/icons/198_hmip-falmot-c12.webp +0 -0
  83. package/data/icons/199_hmip-mio16-pcb.webp +0 -0
  84. package/data/icons/200_hmip-wth-b.webp +0 -0
  85. package/data/icons/201_hmip-stv.webp +0 -0
  86. package/data/icons/202_hmip-wrcr.webp +0 -0
  87. package/data/icons/203_hmip-fsi16.webp +0 -0
  88. package/data/icons/204_hmip-drdi3.webp +0 -0
  89. package/data/icons/205_hmip-drsi4.webp +0 -0
  90. package/data/icons/206_hmip-drbli4.webp +0 -0
  91. package/data/icons/207_hmip-srdo.webp +0 -0
  92. package/data/icons/208_hmip-dsd-pcb.webp +0 -0
  93. package/data/icons/209_hmip-etrv-b-uk.webp +0 -0
  94. package/data/icons/210_hmip-ste2-pcb.webp +0 -0
  95. package/data/icons/211_hmip-drg-dali.webp +0 -0
  96. package/data/icons/211_hmip-drsi1.webp +0 -0
  97. package/data/icons/212_hmip-sfd.webp +0 -0
  98. package/data/icons/212_hmipw-scthd.webp +0 -0
  99. package/data/icons/213_hmip-smo230.webp +0 -0
  100. package/data/icons/213_hmip-wua.webp +0 -0
  101. package/data/icons/214_hmip-dld.webp +0 -0
  102. package/data/icons/215_hmip-psm-pe-2.webp +0 -0
  103. package/data/icons/215_hmip-scth230.webp +0 -0
  104. package/data/icons/216_hmip-etrv-e.webp +0 -0
  105. package/data/icons/216_hmip-psm-ch-2.webp +0 -0
  106. package/data/icons/217_hmip-usbsm.webp +0 -0
  107. package/data/icons/218_hmip-dls.webp +0 -0
  108. package/data/icons/219_hmip-fwi.webp +0 -0
  109. package/data/icons/220_hmip-wsc.webp +0 -0
  110. package/data/icons/221_hmip-sti.webp +0 -0
  111. package/data/icons/221_hmip-wkp.webp +0 -0
  112. package/data/icons/222_hmipw-wgd.webp +0 -0
  113. package/data/icons/223_hmip-mod-wd-vk.webp +0 -0
  114. package/data/icons/224_hmip-etrv-cl.webp +0 -0
  115. package/data/icons/225_hmip-rgbw.webp +0 -0
  116. package/data/icons/226_hmip-esi.webp +0 -0
  117. package/data/icons/227_hmip-lsc.webp +0 -0
  118. package/data/icons/228_elv-sh-sw1-bat.webp +0 -0
  119. package/data/icons/229_elv-sh-bm-s.webp +0 -0
  120. package/data/icons/230_hmip-etrv-f.webp +0 -0
  121. package/data/icons/231_elv-sh-cth.webp +0 -0
  122. package/data/icons/232_elv-sh-taco.webp +0 -0
  123. package/data/icons/233_hmip-flc.webp +0 -0
  124. package/data/icons/234_hmip-etrv-3.webp +0 -0
  125. package/data/icons/235_hmip-psmco.webp +0 -0
  126. package/data/icons/236_elv-sh-sb8.webp +0 -0
  127. package/data/icons/237_elv-sh-ctv.webp +0 -0
  128. package/data/icons/238_elv-sh-cap.webp +0 -0
  129. package/data/icons/239_hmip-wgs-f.webp +0 -0
  130. package/data/icons/240_hmip-wgtc-f.webp +0 -0
  131. package/data/icons/241_hmip-wgt-f.webp +0 -0
  132. package/data/icons/242_elv-sh-smsi.webp +0 -0
  133. package/data/icons/243_elv-sh-crc.webp +0 -0
  134. package/data/icons/244_elv-sh-krco.webp +0 -0
  135. package/data/icons/245_hmip-falmot-c8.webp +0 -0
  136. package/data/icons/246_elv-sh-wsm.webp +0 -0
  137. package/data/icons/247_hmip-hap-2.webp +0 -0
  138. package/data/icons/248_hmip-dlp.webp +0 -0
  139. package/data/icons/249_elv-sh-psmci.webp +0 -0
  140. package/data/icons/250_elv-sh-sps25.webp +0 -0
  141. package/data/icons/252_elv-sh-pti2.webp +0 -0
  142. package/data/icons/253_hmip-udi-smi55.webp +0 -0
  143. package/data/icons/254_hmip-udi-pb2.webp +0 -0
  144. package/data/icons/256_ELV-SH-SMS2.webp +0 -0
  145. package/data/icons/257_ELV-SH-FSI.webp +0 -0
  146. package/data/icons/258_hmip-m-td15.webp +0 -0
  147. package/data/icons/259_elv-sh-dusi.webp +0 -0
  148. package/data/icons/260_hmip-wss.webp +0 -0
  149. package/data/icons/54a_lc-ddc1-pcb.webp +0 -0
  150. package/data/icons/72_hm-rc-brc-h.webp +0 -0
  151. package/data/icons/73_hm-atent.webp +0 -0
  152. package/data/icons/79_hm-lc-dim1pwm-cv.webp +0 -0
  153. package/data/icons/84_hm-rc-4-3.webp +0 -0
  154. package/data/icons/85_hm-rc-sec4-3.webp +0 -0
  155. package/data/icons/86_hm-rc-key4-3.webp +0 -0
  156. package/data/icons/CCU3-1-50.webp +0 -0
  157. package/data/icons/CCU3.webp +0 -0
  158. package/data/icons/hm-coupling-dim.webp +0 -0
  159. package/data/icons/hm-coupling-group.webp +0 -0
  160. package/data/icons/hm-coupling-onoff.webp +0 -0
  161. package/data/icons/hm-coupling-rgb-dim.webp +0 -0
  162. package/data/icons/hm-coupling-rgb.webp +0 -0
  163. package/data/icons/hm-coupling-rgbw-dim.webp +0 -0
  164. package/data/icons/hm-coupling-rgbw.webp +0 -0
  165. package/data/icons/hm-coupling-white-dim.webp +0 -0
  166. package/data/icons/hm-coupling-white.webp +0 -0
  167. package/data/icons/hm-hue_gateway.webp +0 -0
  168. package/data/icons/hm-lightify_gateway.webp +0 -0
  169. package/data/icons/hm_resc-win-pcb-sc.webp +0 -0
  170. package/data/icons/unknown_device.webp +0 -0
  171. package/data/manifest.json +7 -1
  172. package/data/master-metadata.json +1997 -33
  173. package/data/profiles/ACCESS_RECEIVER.json +26 -13
  174. package/data/profiles/ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER.json +3270 -76
  175. package/data/profiles/ACTOR_SECURITY.json +90 -10
  176. package/data/profiles/ACTOR_WINDOW.json +580 -43
  177. package/data/profiles/ALARMACTUATOR.json +564 -37
  178. package/data/profiles/ALARM_COND_SWITCH_RECEIVER.json +36 -4
  179. package/data/profiles/ALARM_SWITCH_VIRTUAL_RECEIVER.json +735 -33
  180. package/data/profiles/ARMING.json +115 -5
  181. package/data/profiles/BLIND.json +1038 -49
  182. package/data/profiles/BLIND_VIRTUAL_RECEIVER.json +4624 -124
  183. package/data/profiles/CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER.json +27 -1
  184. package/data/profiles/DIMMER.json +2233 -71
  185. package/data/profiles/DIMMER_VIRTUAL_RECEIVER.json +5773 -102
  186. package/data/profiles/DIMMER_woLongKeyPress.json +117 -4
  187. package/data/profiles/DOOR_RECEIVER.json +138 -10
  188. package/data/profiles/DUAL_WHITE_BRIGHTNESS.json +305 -11
  189. package/data/profiles/DUAL_WHITE_COLOR.json +730 -23
  190. package/data/profiles/HEATING_KEY_RECEIVER.json +127 -10
  191. package/data/profiles/HMW_BLIND.json +555 -20
  192. package/data/profiles/HMW_DIMMER.json +1337 -47
  193. package/data/profiles/HMW_INPUT_OUTPUT.json +375 -20
  194. package/data/profiles/HMW_SWITCH.json +375 -20
  195. package/data/profiles/JALOUSIE.json +1320 -52
  196. package/data/profiles/KEYMATIC.json +120 -12
  197. package/data/profiles/REMOTECONTROL_RECEIVER.json +10 -2
  198. package/data/profiles/RGBW_AUTOMATIC.json +46 -6
  199. package/data/profiles/RGBW_COLOR.json +14 -4
  200. package/data/profiles/SERVO_VIRTUAL_RECEIVER.json +117 -3
  201. package/data/profiles/SHUTTER_VIRTUAL_RECEIVER.json +4263 -128
  202. package/data/profiles/SIGNAL_CHIME.json +195 -14
  203. package/data/profiles/SIGNAL_CHIMEM.json +195 -14
  204. package/data/profiles/SIGNAL_LED.json +195 -14
  205. package/data/profiles/SIGNAL_LEDM.json +195 -14
  206. package/data/profiles/SIMPLE_SWITCH_RECEIVER.json +113 -9
  207. package/data/profiles/STATUS_INDICATOR.json +1073 -45
  208. package/data/profiles/SWITCH.json +1412 -60
  209. package/data/profiles/SWITCH_PANIC.json +315 -13
  210. package/data/profiles/SWITCH_SENSOR.json +243 -11
  211. package/data/profiles/SWITCH_VIRTUAL_RECEIVER.json +3963 -131
  212. package/data/profiles/UNIVERSAL_ACTOR.json +4319 -90
  213. package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_LSC.json +6094 -196
  214. package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_PWM.json +4920 -101
  215. package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_RGB(W).json +2353 -80
  216. package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_RGBW_DALI.json +6116 -176
  217. package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_TW.json +3844 -104
  218. package/data/profiles/VIRTUAL_DIMMER.json +305 -11
  219. package/data/profiles/VIRTUAL_DUAL_WHITE_COLOR.json +730 -23
  220. package/data/profiles/WATER_SWITCH_VIRTUAL_RECEIVER.json +3903 -128
  221. package/data/profiles/WINDOW_DRIVE_RECEIVER.json +220 -8
  222. package/data/profiles/WINMATIC.json +505 -23
  223. package/dist/cli.js +24 -0
  224. package/dist/occulite.d.ts +86 -0
  225. package/dist/occulite.js +140 -0
  226. package/dist/options.d.ts +37 -6
  227. package/dist/options.js +49 -6
  228. package/dist/server.d.ts +47 -4
  229. package/dist/server.js +237 -27
  230. package/dist/sessions.d.ts +9 -1
  231. package/dist/sessions.js +8 -2
  232. package/dist/testSupport.js +9 -2
  233. package/node_modules/@homematic-manager/backend/dist/api/backend.d.ts +33 -0
  234. package/node_modules/@homematic-manager/backend/dist/api/backend.js +558 -46
  235. package/node_modules/@homematic-manager/backend/dist/cache/names.d.ts +9 -0
  236. package/node_modules/@homematic-manager/backend/dist/cache/names.js +21 -0
  237. package/node_modules/@homematic-manager/backend/dist/config/defaults.js +10 -1
  238. package/node_modules/@homematic-manager/backend/dist/config/store.d.ts +28 -1
  239. package/node_modules/@homematic-manager/backend/dist/config/store.js +59 -3
  240. package/node_modules/@homematic-manager/backend/dist/devices/installMode.js +9 -0
  241. package/node_modules/@homematic-manager/backend/dist/errors.d.ts +18 -0
  242. package/node_modules/@homematic-manager/backend/dist/errors.js +51 -2
  243. package/node_modules/@homematic-manager/backend/dist/images/deviceImages.d.ts +6 -0
  244. package/node_modules/@homematic-manager/backend/dist/images/deviceImages.js +38 -2
  245. package/node_modules/@homematic-manager/backend/dist/index.d.ts +3 -1
  246. package/node_modules/@homematic-manager/backend/dist/index.js +4 -1
  247. package/node_modules/@homematic-manager/backend/dist/interfaces/manager.d.ts +70 -8
  248. package/node_modules/@homematic-manager/backend/dist/interfaces/manager.js +333 -44
  249. package/node_modules/@homematic-manager/backend/dist/meta/client.d.ts +120 -0
  250. package/node_modules/@homematic-manager/backend/dist/meta/client.js +307 -0
  251. package/node_modules/@homematic-manager/backend/dist/meta/credentials.d.ts +38 -0
  252. package/node_modules/@homematic-manager/backend/dist/meta/credentials.js +53 -0
  253. package/node_modules/@homematic-manager/backend/dist/meta/index.d.ts +12 -0
  254. package/node_modules/@homematic-manager/backend/dist/meta/index.js +12 -0
  255. package/node_modules/@homematic-manager/backend/dist/meta/localProvider.d.ts +47 -0
  256. package/node_modules/@homematic-manager/backend/dist/meta/localProvider.js +119 -0
  257. package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.d.ts +79 -0
  258. package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.js +305 -0
  259. package/node_modules/@homematic-manager/backend/dist/meta/provider.d.ts +68 -0
  260. package/node_modules/@homematic-manager/backend/dist/meta/provider.js +19 -0
  261. package/node_modules/@homematic-manager/backend/dist/meta/regaProvider.d.ts +84 -0
  262. package/node_modules/@homematic-manager/backend/dist/meta/regaProvider.js +403 -0
  263. package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +137 -0
  264. package/node_modules/@homematic-manager/backend/dist/meta/service.js +412 -0
  265. package/node_modules/@homematic-manager/backend/dist/rega/client.d.ts +11 -0
  266. package/node_modules/@homematic-manager/backend/dist/rega/client.js +13 -0
  267. package/node_modules/@homematic-manager/backend/dist/rega/scripts.d.ts +51 -0
  268. package/node_modules/@homematic-manager/backend/dist/rega/scripts.js +171 -0
  269. package/node_modules/@homematic-manager/backend/dist/rpc/client.d.ts +18 -5
  270. package/node_modules/@homematic-manager/backend/dist/rpc/client.js +11 -5
  271. package/node_modules/@homematic-manager/backend/dist/rpc/log.d.ts +86 -0
  272. package/node_modules/@homematic-manager/backend/dist/rpc/log.js +241 -0
  273. package/node_modules/@homematic-manager/backend/dist/rpc/origin.d.ts +30 -0
  274. package/node_modules/@homematic-manager/backend/dist/rpc/origin.js +35 -0
  275. package/node_modules/@homematic-manager/backend/dist/rpc/server.d.ts +12 -0
  276. package/node_modules/@homematic-manager/backend/dist/rpc/server.js +37 -5
  277. package/node_modules/@homematic-manager/backend/dist/util/net.d.ts +86 -1
  278. package/node_modules/@homematic-manager/backend/dist/util/net.js +223 -9
  279. package/node_modules/@homematic-manager/backend/package.json +1 -1
  280. package/node_modules/@homematic-manager/core/dist/api/types.d.ts +397 -10
  281. package/node_modules/@homematic-manager/core/dist/api/types.js +23 -2
  282. package/node_modules/@homematic-manager/core/dist/data/memory.d.ts +3 -1
  283. package/node_modules/@homematic-manager/core/dist/data/memory.js +3 -0
  284. package/node_modules/@homematic-manager/core/dist/data/types.d.ts +60 -0
  285. package/node_modules/@homematic-manager/core/dist/data/types.js +2 -0
  286. package/node_modules/@homematic-manager/core/dist/devices/index.d.ts +19 -0
  287. package/node_modules/@homematic-manager/core/dist/devices/index.js +53 -0
  288. package/node_modules/@homematic-manager/core/dist/easymodes/engine.d.ts +3 -1
  289. package/node_modules/@homematic-manager/core/dist/easymodes/engine.js +1 -0
  290. package/node_modules/@homematic-manager/core/dist/easymodes/form.d.ts +43 -0
  291. package/node_modules/@homematic-manager/core/dist/easymodes/form.js +84 -0
  292. package/node_modules/@homematic-manager/core/dist/i18n/messages.js +61 -0
  293. package/node_modules/@homematic-manager/core/dist/index.d.ts +3 -0
  294. package/node_modules/@homematic-manager/core/dist/index.js +4 -0
  295. package/node_modules/@homematic-manager/core/dist/interfaces/table.d.ts +23 -2
  296. package/node_modules/@homematic-manager/core/dist/interfaces/table.js +18 -1
  297. package/node_modules/@homematic-manager/core/dist/meta/document.d.ts +71 -0
  298. package/node_modules/@homematic-manager/core/dist/meta/document.js +260 -0
  299. package/node_modules/@homematic-manager/core/dist/meta/follow.d.ts +27 -0
  300. package/node_modules/@homematic-manager/core/dist/meta/follow.js +180 -0
  301. package/node_modules/@homematic-manager/core/dist/meta/index.d.ts +16 -0
  302. package/node_modules/@homematic-manager/core/dist/meta/index.js +16 -0
  303. package/node_modules/@homematic-manager/core/dist/meta/paths.d.ts +96 -0
  304. package/node_modules/@homematic-manager/core/dist/meta/paths.js +182 -0
  305. package/node_modules/@homematic-manager/core/dist/meta/slug.d.ts +22 -0
  306. package/node_modules/@homematic-manager/core/dist/meta/slug.js +59 -0
  307. package/node_modules/@homematic-manager/core/dist/meta/store.d.ts +147 -0
  308. package/node_modules/@homematic-manager/core/dist/meta/store.js +537 -0
  309. package/node_modules/@homematic-manager/core/dist/meta/tree.d.ts +19 -0
  310. package/node_modules/@homematic-manager/core/dist/meta/tree.js +53 -0
  311. package/node_modules/@homematic-manager/core/dist/meta/types.d.ts +143 -0
  312. package/node_modules/@homematic-manager/core/dist/meta/types.js +63 -0
  313. package/node_modules/@homematic-manager/core/dist/meta/view.d.ts +50 -0
  314. package/node_modules/@homematic-manager/core/dist/meta/view.js +103 -0
  315. package/node_modules/@homematic-manager/core/dist/rpc/methods.d.ts +10 -0
  316. package/node_modules/@homematic-manager/core/dist/rpc/methods.js +51 -1
  317. package/node_modules/@homematic-manager/core/dist/rpc/text.d.ts +22 -0
  318. package/node_modules/@homematic-manager/core/dist/rpc/text.js +46 -0
  319. package/node_modules/@homematic-manager/core/dist/rssi/index.d.ts +125 -13
  320. package/node_modules/@homematic-manager/core/dist/rssi/index.js +146 -29
  321. package/node_modules/@homematic-manager/core/dist/serviceMessages/index.d.ts +28 -2
  322. package/node_modules/@homematic-manager/core/dist/serviceMessages/index.js +44 -6
  323. package/node_modules/@homematic-manager/core/package.json +1 -1
  324. package/package.json +3 -3
  325. package/ui/assets/index-D-NsbBVS.css +1 -0
  326. package/ui/assets/index-DNUNMBbb.js +16 -0
  327. package/ui/favicon.ico +0 -0
  328. package/ui/index.html +3 -2
  329. package/node_modules/@homematic-manager/backend/dist/write/log.d.ts +0 -51
  330. package/node_modules/@homematic-manager/backend/dist/write/log.js +0 -151
  331. package/ui/assets/index-lB-HxsKR.js +0 -13
  332. package/ui/assets/index-qbhgN2pA.css +0 -1
@@ -14,7 +14,7 @@
14
14
  * main process with it, issue #127).
15
15
  */
16
16
  import path from 'node:path';
17
- import { RPC_METHOD_NAMES, RSSI_UNKNOWN, countsAsServiceMessage, isAcknowledgeable, maintenanceAddress, mergeMethodHelp, methodsFor, } from '@homematic-manager/core';
17
+ import { RPC_METHOD_NAMES, RSSI_UNKNOWN, countsAsServiceMessage, isAcknowledgeable, isKnownInterface, maintenanceAddress, mergeMethodHelp, methodsFor, callbackPinOption, normaliseDescription, repairMisdecodedUtf8, } from '@homematic-manager/core';
18
18
  import { CacheStore } from '../cache/store.js';
19
19
  import { ConfigStore } from '../config/store.js';
20
20
  import { LinkTemplateStore } from '../config/linkTemplates.js';
@@ -23,12 +23,14 @@ import { DataFileServer } from '../data/files.js';
23
23
  import { installModeCalls } from '../devices/installMode.js';
24
24
  import { discoverCcus } from '../discovery/discover.js';
25
25
  import { BackendError, configError, connectionError, errorMessage, internalError, isMethodUnsupported, validationError, } from '../errors.js';
26
- import { InterfaceManager, firstBidcosInterfaceAddress } from '../interfaces/manager.js';
26
+ import { InterfaceManager, LOOPBACK_IP, callbackBindHost, firstBidcosInterfaceAddress, } from '../interfaces/manager.js';
27
+ import { MetaService } from '../meta/service.js';
27
28
  import { RegaService } from '../rega/client.js';
28
29
  import { listDevicesAnswer } from '../rpc/server.js';
29
30
  import { ApiEventEmitter } from '../util/emitter.js';
30
- import { WriteLog } from '../write/log.js';
31
- import { isWriteMethod } from '../write/log.js';
31
+ import { describeCallbackAddresses, staticNetwork, systemNetwork } from '../util/net.js';
32
+ import { RpcLog, isWriteMethod } from '../rpc/log.js';
33
+ import { currentOrigin, runWithOrigin } from '../rpc/origin.js';
32
34
  import { ParamsetWriter } from '../write/paramset.js';
33
35
  import { ConfigRepair } from '../write/repair.js';
34
36
  import { WriteQueue } from '../write/queue.js';
@@ -36,13 +38,26 @@ import { WriteQueue } from '../write/queue.js';
36
38
  export const SERVICE_MESSAGE_POLL_MS = 300_000;
37
39
  /** How long the HmIP `getParamset(:0, VALUES)` sweep waits for the device list to settle. */
38
40
  export const HMIP_SWEEP_DELAY_MS = 1000;
41
+ /**
42
+ * An HmIP interface, by name or by interface type - the same rule the UI's settings dialog uses to
43
+ * count the messages it asks about (task 34), so the question and the writes agree.
44
+ */
45
+ function isHmipInterface(name, type) {
46
+ return /hmip/i.test(name) || /hmip/i.test(type);
47
+ }
48
+ /** Task 38: how a log line names a callback field. */
49
+ const CALLBACK_FIELD_WORDS = {
50
+ ip: 'callback address',
51
+ xmlrpcPort: 'XML-RPC callback port',
52
+ binrpcPort: 'BIN-RPC callback port',
53
+ };
39
54
  /** Implements the whole contract. `open()` loads, `start()` connects, `stop()` disconnects. */
40
55
  export class Backend {
41
56
  events;
42
57
  #options;
43
58
  #config;
44
59
  #queue;
45
- #writeLog;
60
+ #rpcLog;
46
61
  #unknownMethodsSeen = new Set();
47
62
  #writer;
48
63
  #repair;
@@ -54,6 +69,19 @@ export class Backend {
54
69
  #caches;
55
70
  #manager;
56
71
  #rega;
72
+ /** D-40: the metadata store of this connection - this profile's own, or an openccu-lite box. */
73
+ #meta;
74
+ /** D-40: the session a host with a login knows about; writes to the box go out as this one. */
75
+ #metaSession;
76
+ /**
77
+ * D-40: the detection, in flight.
78
+ *
79
+ * The probe is **not** awaited by `#connect`: a host that swallows packets on port 80 - a
80
+ * firewall, a CCU that is off, a WSL loopback - would otherwise hold the whole connection up
81
+ * for the detection timeout while the interfaces are already there. Everything that needs the
82
+ * store waits for this promise instead, and the UI hears about it through `meta.changed`.
83
+ */
84
+ #metaReady;
57
85
  #serviceMessageTimer;
58
86
  #hmipSweepTimer;
59
87
  #hmipSweepRunning = false;
@@ -65,7 +93,23 @@ export class Backend {
65
93
  * table - and it is emptied whenever the connection is rebuilt.
66
94
  */
67
95
  #noServiceMessages = new Set();
96
+ /**
97
+ * B-26 (#158): interfaces whose last `getServiceMessages` failed for a reason that may pass - a
98
+ * timeout, a process that is restarting. The failure is logged when the interface lands in
99
+ * here and not again until a call succeeds; before, the five-minute poll logged it every round.
100
+ */
101
+ #serviceMessageFailures = new Set();
102
+ /**
103
+ * B-26 (#158): the `system.listMethods` answer of a user-defined interface, asked once and
104
+ * dropped when the interface subscribes again - a re-`init` may be a restarted, updated process.
105
+ * `undefined` inside means "no usable list", and the method is then tried once, as before.
106
+ */
107
+ #listedMethods = new Map();
108
+ /** B-26: interfaces that do not have `system.listMethods` itself; kept for the connection. */
109
+ #noListMethods = new Set();
68
110
  #idleTimer;
111
+ /** Task 38: the saved callback values a pinned one replaces are logged by the first `start()` only. */
112
+ #pinsReported = false;
69
113
  constructor(options, config, caches) {
70
114
  this.#options = options;
71
115
  this.#config = config;
@@ -82,14 +126,14 @@ export class Backend {
82
126
  this.#queue = new WriteQueue({
83
127
  paceFor: (interfaceName) => writePaceFor(interfaceName, this.#config.connection.writePaceMs),
84
128
  });
85
- this.#writeLog = new WriteLog({
129
+ this.#rpcLog = new RpcLog({
86
130
  file: config.cacheFile('write-log.json'),
87
131
  rpcLogFolder: config.connection.rpcLogFolder,
88
132
  onAppended: (entry) => {
89
- this.events.emit('writeLog.appended', entry);
133
+ this.events.emit('rpcLog.appended', entry);
90
134
  },
91
135
  onError: (error) => {
92
- this.#notice('warn', `write log: ${errorMessage(error)}`);
136
+ this.#notice('warn', `RPC log: ${errorMessage(error)}`);
93
137
  },
94
138
  ...(options.cacheWriteDelayMs === undefined ? {} : { writeDelayMs: options.cacheWriteDelayMs }),
95
139
  });
@@ -123,7 +167,7 @@ export class Backend {
123
167
  });
124
168
  await caches.load();
125
169
  const backend = new Backend(options, config, caches);
126
- await backend.#writeLog.load();
170
+ await backend.#rpcLog.load();
127
171
  await backend.#linkTemplates.load();
128
172
  return backend;
129
173
  }
@@ -137,6 +181,14 @@ export class Backend {
137
181
  if (this.#config.importedFromLegacy) {
138
182
  this.#notice('info', 'the configuration of Homematic Manager 2.x was imported');
139
183
  }
184
+ if (!this.#pinsReported) {
185
+ // Task 38: once per process - the value in config.json stays, it only has no effect
186
+ this.#pinsReported = true;
187
+ for (const { field, saved, pinned } of this.#config.ignoredCallback) {
188
+ this.#notice('info', `callback: the saved ${CALLBACK_FIELD_WORDS[field]} ${String(saved)} is ignored, ` +
189
+ `${callbackPinOption(field)} sets ${String(pinned)}`);
190
+ }
191
+ }
140
192
  const problems = validateConnection(this.#config.connection);
141
193
  if (problems.length > 0) {
142
194
  this.#notice('info', `not connecting: ${problems.join(', ')}`);
@@ -152,8 +204,17 @@ export class Backend {
152
204
  await this.#manager?.stop();
153
205
  this.#manager = undefined;
154
206
  this.#rega = undefined;
207
+ // D-40: before the caches are flushed, and awaited even when it is only the detection that
208
+ // is still in flight. A store that finishes loading after this would apply its names, ask
209
+ // for a cache write and put a file back into a directory the caller has already taken
210
+ // apart - which is how CI found this, as an ENOTEMPTY in a suite that has nothing to do
211
+ // with metadata.
212
+ await this.#metaReady?.catch(() => undefined);
213
+ this.#metaReady = undefined;
214
+ await this.#meta?.stop();
215
+ this.#meta = undefined;
155
216
  await this.#caches.flush();
156
- await this.#writeLog.flush();
217
+ await this.#rpcLog.flush();
157
218
  this.events.clear();
158
219
  }
159
220
  /**
@@ -176,7 +237,7 @@ export class Backend {
176
237
  }
177
238
  if (this.#sessions > 0) {
178
239
  if (previous === 0) {
179
- void this.#resubscribe();
240
+ void runWithOrigin('background', () => this.#resubscribe());
180
241
  }
181
242
  return;
182
243
  }
@@ -186,7 +247,7 @@ export class Backend {
186
247
  }
187
248
  const timer = setTimeout(() => {
188
249
  this.#idleTimer = undefined;
189
- void this.#unsubscribeIdle();
250
+ void runWithOrigin('background', () => this.#unsubscribeIdle());
190
251
  }, grace);
191
252
  if (typeof timer.unref === 'function') {
192
253
  timer.unref();
@@ -219,7 +280,9 @@ export class Backend {
219
280
  */
220
281
  async request(method, ...params) {
221
282
  try {
222
- return await this.#dispatch(method, params);
283
+ // Task 48: everything a request awaits is a UI action for the RPC log; the console's
284
+ // own call is the console's. Whatever runs outside a request is background work.
285
+ return await runWithOrigin(method === 'rpc.call' ? 'console' : 'ui', () => this.#dispatch(method, params));
223
286
  }
224
287
  catch (error) {
225
288
  throw error instanceof BackendError ? error : internalError(errorMessage(error), error);
@@ -240,11 +303,13 @@ export class Backend {
240
303
  case 'config.get':
241
304
  return this.#configWithDetected();
242
305
  case 'config.set':
243
- return this.#setConfig(p[0]);
306
+ return this.#setConfig(p[0], params[1]);
244
307
  case 'config.discover':
245
308
  return this.#discover();
246
309
  case 'config.clearCaches':
247
310
  return this.#clearCaches();
311
+ case 'config.callbackAddresses':
312
+ return this.#callbackAddresses(p[0]);
248
313
  case 'interfaces.list':
249
314
  return this.#manager?.states() ?? [];
250
315
  case 'interfaces.reconnect':
@@ -284,6 +349,51 @@ export class Backend {
284
349
  return this.#caches.names.all();
285
350
  case 'names.set':
286
351
  return this.#setNames(p[0]);
352
+ case 'meta.state':
353
+ return this.#metaState();
354
+ case 'meta.get':
355
+ return this.#metaSnapshot();
356
+ case 'meta.enums':
357
+ return (await this.#requireMeta()).enums();
358
+ case 'meta.objects':
359
+ return (await this.#requireMeta()).objects();
360
+ case 'meta.setMembership':
361
+ await (await this.#requireMeta()).setMembership(p[0]);
362
+ return null;
363
+ case 'meta.assign':
364
+ await (await this.#requireMeta()).assign(p[0], p[1], p[2]);
365
+ return null;
366
+ case 'meta.enum.create':
367
+ await (await this.#requireMeta()).createEnum(p[0], p[1]);
368
+ return null;
369
+ case 'meta.enum.update':
370
+ await (await this.#requireMeta()).updateEnum(p[0], p[1]);
371
+ return null;
372
+ case 'meta.enum.delete':
373
+ await (await this.#requireMeta()).deleteEnum(p[0], p[1] === true);
374
+ return null;
375
+ case 'meta.node.create':
376
+ return (await this.#requireMeta()).createNode(p[0],
377
+ // absent means "at the root"; see the contract for why it is not `null` here.
378
+ // Read from `params` and not from `p`, whose slots are `never`: this is the one
379
+ // place that has to tell "absent" from a value, and `never` cannot.
380
+ params[1] ?? null, p[2], params[3] ?? {});
381
+ case 'meta.node.update':
382
+ await (await this.#requireMeta()).updateNode(p[0], params[1]);
383
+ return null;
384
+ case 'meta.node.delete':
385
+ await (await this.#requireMeta()).deleteNode(p[0], p[1] === true);
386
+ return null;
387
+ case 'meta.refresh': {
388
+ const meta = await this.#requireMeta();
389
+ await meta.refresh();
390
+ return meta.state();
391
+ }
392
+ case 'meta.export':
393
+ return (await this.#requireMeta()).document();
394
+ case 'meta.import':
395
+ await (await this.#requireMeta()).import(p[0], params[1] ?? 'replace');
396
+ return null;
287
397
  case 'paramset.get':
288
398
  return this.#getParamset(p[0], p[1], p[2]);
289
399
  case 'paramset.description':
@@ -348,6 +458,8 @@ export class Backend {
348
458
  return this.#resetUnreach(p[0], p[1]);
349
459
  case 'serviceMessages.list':
350
460
  return this.#serviceMessages(p[0]);
461
+ case 'serviceMessages.refresh':
462
+ return this.#refreshAllServiceMessages(p[0]);
351
463
  case 'serviceMessages.ack':
352
464
  return this.#acknowledge(p[0], p[1], p[2]);
353
465
  case 'events.recent':
@@ -361,10 +473,10 @@ export class Backend {
361
473
  return this.#methods(p[0]);
362
474
  case 'write.cancel':
363
475
  return this.#queue.cancel(p[0]);
364
- case 'writeLog.list':
365
- return this.#writeLog.list(p[0]);
366
- case 'writeLog.clear':
367
- this.#writeLog.clear();
476
+ case 'rpcLog.list':
477
+ return this.#rpcLog.list(p[0]);
478
+ case 'rpcLog.clear':
479
+ this.#rpcLog.clear();
368
480
  return null;
369
481
  case 'data.file':
370
482
  return this.#files.read(p[0]);
@@ -386,9 +498,20 @@ export class Backend {
386
498
  }
387
499
  return this.#manager;
388
500
  }
389
- async #connect() {
501
+ /**
502
+ * Task 48: the connection is background work even when a `config.set` from the settings dialog
503
+ * asks for it - the watchdog, the callback servers and the polling timers it creates would
504
+ * otherwise inherit that request's origin for the rest of the session.
505
+ */
506
+ #connect() {
507
+ return runWithOrigin('background', () => this.#connectNow());
508
+ }
509
+ async #connectNow() {
390
510
  const connection = this.#config.connection;
391
511
  this.#noServiceMessages.clear();
512
+ this.#serviceMessageFailures.clear();
513
+ this.#listedMethods.clear();
514
+ this.#noListMethods.clear();
392
515
  const manager = (this.#options.createInterfaceManager ?? ((options) => new InterfaceManager(options)))({
393
516
  connection,
394
517
  handler: this.#callbackHandler(),
@@ -398,16 +521,23 @@ export class Backend {
398
521
  onNotice: (level, message, interfaceName) => {
399
522
  this.#notice(level, message, interfaceName);
400
523
  },
401
- onConnected: (interfaceName) => this.#onInterfaceConnected(interfaceName),
524
+ onConnected: (interfaceName) => runWithOrigin('background', () => this.#onInterfaceConnected(interfaceName)),
402
525
  onCall: (record) => {
403
526
  this.#onCall(record);
404
527
  },
528
+ originOf: currentOrigin,
405
529
  ...(this.#options.callbackHost === undefined ? {} : { callbackHost: this.#options.callbackHost }),
530
+ ...(this.#options.defaultCallbackPorts === undefined
531
+ ? {}
532
+ : { defaultCallbackPorts: this.#options.defaultCallbackPorts }),
533
+ ...(this.#config.callbackPins === undefined ? {} : { callbackPins: this.#config.callbackPins }),
406
534
  ...(this.#options.rpcTimeoutMs === undefined ? {} : { rpcTimeoutMs: this.#options.rpcTimeoutMs }),
407
535
  ...(this.#options.watchdogIntervalMs === undefined
408
536
  ? {}
409
537
  : { watchdogIntervalMs: this.#options.watchdogIntervalMs }),
410
538
  ...(this.#options.localAddresses === undefined ? {} : { localAddresses: this.#options.localAddresses }),
539
+ ...(this.#options.network === undefined ? {} : { network: this.#options.network }),
540
+ ...(this.#keepsConfiguredCallbackIp() ? { keepConfiguredCallbackIp: true } : {}),
411
541
  ...(this.#options.now === undefined ? {} : { now: this.#options.now }),
412
542
  ...this.#options.interfaceManagerOptions,
413
543
  });
@@ -444,6 +574,7 @@ export class Backend {
444
574
  this.#caches.saveNames();
445
575
  this.events.emit('names.changed', this.#caches.names.all());
446
576
  }
577
+ this.#metaReady = this.#startMeta(connection);
447
578
  this.#startServiceMessagePolling();
448
579
  }
449
580
  async #disconnect() {
@@ -452,8 +583,104 @@ export class Backend {
452
583
  await this.#manager?.stop();
453
584
  this.#manager = undefined;
454
585
  this.#rega = undefined;
586
+ // the detection may still be in flight; stopping a provider that is not there yet would
587
+ // leave its event stream running behind the disconnect
588
+ await this.#metaReady?.catch(() => undefined);
589
+ this.#metaReady = undefined;
590
+ await this.#meta?.stop();
591
+ this.#meta = undefined;
455
592
  this.#methodCache.clear();
456
593
  }
594
+ /**
595
+ * D-40: picks the metadata provider for this connection and starts it.
596
+ *
597
+ * After the interfaces, because the ref of an address (`<interface>.<address>`) is resolved
598
+ * from the device caches and those are filled by the `listDevices` sweep the interfaces do on
599
+ * connect. Before that the store's names are still applied - they are keyed by ref, and the
600
+ * address half of a ref never needs an interface to be readable.
601
+ */
602
+ async #startMeta(connection) {
603
+ try {
604
+ const meta = await MetaService.create({
605
+ connection,
606
+ dataDir: this.#options.dataDir,
607
+ cacheDir: this.#config.cacheDir,
608
+ names: this.#caches.names,
609
+ interfaceOf: (address) => this.#interfaceOf(address),
610
+ // task 27: ReGa as the store of rooms and functions on a CCU. Read through the
611
+ // service that is current at call time - a reconnect replaces it.
612
+ rega: this.#rega === undefined || !connection.rega
613
+ ? undefined
614
+ : {
615
+ available: this.#rega.available,
616
+ exec: (script) => this.#requireRega().exec(script),
617
+ },
618
+ onChanged: () => {
619
+ this.#onMetaChanged();
620
+ },
621
+ onStateChanged: (state) => {
622
+ this.events.emit('meta.changed', state);
623
+ },
624
+ onNotice: (level, message) => {
625
+ this.#notice(level, message);
626
+ },
627
+ ...this.#options.metaOptions,
628
+ });
629
+ if (this.#stopped) {
630
+ // stopped while the box was being probed; starting the provider now would open an
631
+ // event stream nobody will ever close
632
+ return;
633
+ }
634
+ meta.setSessionCredential(this.#metaSession);
635
+ this.#meta = meta;
636
+ await meta.start();
637
+ this.#onMetaChanged();
638
+ }
639
+ catch (error) {
640
+ // D-2 in its metadata shape: a store that cannot be opened costs the taxonomy, never
641
+ // the application
642
+ this.#notice('warn', `the metadata store could not be opened: ${errorMessage(error)}`);
643
+ }
644
+ }
645
+ #requireRega() {
646
+ if (!this.#rega) {
647
+ throw configError('ReGa is not connected');
648
+ }
649
+ return this.#rega;
650
+ }
651
+ /** The store changed - locally, or on the box because somebody else edited it. */
652
+ #onMetaChanged() {
653
+ const meta = this.#meta;
654
+ if (!meta || this.#stopped) {
655
+ return;
656
+ }
657
+ this.#caches.saveNames();
658
+ this.events.emit('names.changed', this.#caches.names.all());
659
+ this.events.emit('meta.enums.changed', meta.enums());
660
+ this.events.emit('meta.objects.changed', meta.objects());
661
+ }
662
+ /** Which interface reports an address; the ref of the metadata store is built from it. */
663
+ #interfaceOf(address) {
664
+ const device = address.split(':')[0] ?? address;
665
+ for (const interfaceName of this.#caches.devices.interfaces()) {
666
+ if (this.#caches.devices.get(interfaceName, address) ?? this.#caches.devices.get(interfaceName, device)) {
667
+ return interfaceName;
668
+ }
669
+ }
670
+ return undefined;
671
+ }
672
+ /**
673
+ * D-40: the session of the person looking at the page, for the writes to an openccu-lite box.
674
+ *
675
+ * The shell of the box hands an addon page the user's session as `?sid=@xxxxxxxxxx@`, and that
676
+ * session is a valid credential for the metadata API. The addon reads with the box's local
677
+ * token, which is read-only by design, and writes as the user - so nothing renames a device
678
+ * unless a person asked for it. Hosts without a login never call this.
679
+ */
680
+ noteMetaSession(sid) {
681
+ this.#metaSession = sid;
682
+ this.#meta?.setSessionCredential(sid);
683
+ }
457
684
  #clearTimers() {
458
685
  if (this.#idleTimer !== undefined) {
459
686
  clearTimeout(this.#idleTimer);
@@ -470,6 +697,8 @@ export class Backend {
470
697
  }
471
698
  /** Fills the caches of an interface that has just subscribed. */
472
699
  async #onInterfaceConnected(interfaceName) {
700
+ // B-26: a (re-)`init` may be a restarted process, so its method list is asked again
701
+ this.#listedMethods.delete(interfaceName);
473
702
  try {
474
703
  await this.#refreshDevices(interfaceName);
475
704
  }
@@ -589,13 +818,16 @@ export class Backend {
589
818
  * a device that is unreachable is not going to take a write either, which is the normal case
590
819
  * here and not worth an error dialog.
591
820
  */
592
- #noteUnreach(interfaceName, address, datapoint, value) {
821
+ #noteUnreach(interfaceName, address, datapoint, value, options = {}) {
593
822
  if (!this.#caches.unreach.note(interfaceName, address, datapoint, value, this.#now())) {
594
823
  return;
595
824
  }
596
825
  this.#caches.saveUnreach();
597
826
  this.events.emit('unreach.changed', this.#caches.unreach.list());
598
- if (datapoint === 'STICKY_UNREACH' && value === true && this.#config.connection.autoAckStickyUnreach === true) {
827
+ if (options.acknowledge !== false &&
828
+ datapoint === 'STICKY_UNREACH' &&
829
+ value === true &&
830
+ this.#config.connection.autoAckStickyUnreach === true) {
599
831
  void this.#acknowledge(interfaceName, address, datapoint).catch((error) => {
600
832
  this.#notice('info', `${address}: STICKY_UNREACH could not be acknowledged automatically: ${errorMessage(error)}`, interfaceName);
601
833
  });
@@ -617,7 +849,7 @@ export class Backend {
617
849
  * RPC
618
850
  */
619
851
  #onCall(record) {
620
- this.#writeLog.append(record);
852
+ this.#rpcLog.append(record);
621
853
  }
622
854
  /** A read: straight to the interface, never queued. */
623
855
  async #read(interfaceName, method, params) {
@@ -626,19 +858,45 @@ export class Backend {
626
858
  /** A write: through the paced queue of that interface. */
627
859
  async #write(interfaceName, method, params) {
628
860
  const client = this.#requireManager().client(interfaceName);
629
- return this.#queue.enqueue(interfaceName, () => client.call(method, params));
861
+ // Task 48: the queue runs a task from the timer that drained it, which is the *previous*
862
+ // task's context; the origin is the one of whoever enqueued, so it is taken here.
863
+ const origin = currentOrigin();
864
+ return this.#queue.enqueue(interfaceName, () => client.call(method, params, { origin }));
630
865
  }
631
866
  /*
632
867
  * configuration
633
868
  */
634
869
  #configWithDetected() {
635
- return this.#config.config;
870
+ return this.#withHostFacts(this.#config.config);
871
+ }
872
+ /**
873
+ * What the host adds to the stored configuration before anybody sees it: task 35's callback
874
+ * ports a `0` stands for. Added to every `AppConfig` that leaves the backend - `config.get`,
875
+ * `config.set` and both `config.changed` events - so the dialog never loses the hint on a save.
876
+ */
877
+ #withHostFacts(config) {
878
+ const ports = this.#options.defaultCallbackPorts;
879
+ const pins = this.#config.callbackPins;
880
+ // Task 38: a container whose callback servers listen on the loopback only has nothing to publish
881
+ const bindHost = this.#options.callbackHost ?? callbackBindHost(config.connection);
882
+ const publish = this.#options.inContainer === true && bindHost !== LOOPBACK_IP;
883
+ return {
884
+ ...config,
885
+ ...(ports === undefined ? {} : { callbackDefaultPorts: { xmlrpc: ports.xmlrpc, binrpc: ports.binrpc } }),
886
+ ...(pins === undefined ? {} : { callbackPinned: { ...pins } }),
887
+ ...(publish ? { publishCallbackPorts: true } : {}),
888
+ };
636
889
  }
637
- async #setConfig(connection) {
890
+ async #setConfig(connection, options) {
638
891
  const previousHost = this.#config.connection.host;
892
+ // Task 34 (#147, D-42): the messages the settings dialog asked about, taken before the
893
+ // reconnect, and only on the save that switches the auto-acknowledge from off to on.
894
+ const existing = options?.acknowledgeExisting === true && this.#config.connection.autoAckStickyUnreach !== true
895
+ ? this.#stickyUnreachMessages()
896
+ : [];
639
897
  await this.#disconnect();
640
- const config = await this.#config.setConnection(connection);
641
- this.#writeLog.setRpcLogFolder(config.connection.rpcLogFolder);
898
+ const config = this.#withHostFacts(await this.#config.setConnection(connection));
899
+ this.#rpcLog.setRpcLogFolder(config.connection.rpcLogFolder);
642
900
  if (config.connection.host !== previousHost) {
643
901
  await this.#caches.flush();
644
902
  this.#caches = new CacheStore({
@@ -653,14 +911,82 @@ export class Backend {
653
911
  this.events.emit('config.changed', config);
654
912
  if (!this.#stopped && validateConnection(config.connection).length === 0) {
655
913
  await this.#connect();
914
+ // after the reconnect, because a write needs the interface; not awaited, so the
915
+ // settings dialog closes as quickly as ever and the writes show up in the RPC log
916
+ if (existing.length > 0 &&
917
+ config.connection.autoAckStickyUnreach === true &&
918
+ config.connection.host === previousHost) {
919
+ void this.#acknowledgeExisting(existing);
920
+ }
656
921
  }
657
922
  return config;
658
923
  }
924
+ /**
925
+ * Task 34: the `STICKY_UNREACH` messages in the list, without HmIP's.
926
+ *
927
+ * D-42 says nothing is acknowledged on HmIP: hmipserver has no such flag on real hardware, and
928
+ * where one appears anyway (a simulator, a future firmware) this is not the switch that should
929
+ * start writing to those devices.
930
+ */
931
+ #stickyUnreachMessages() {
932
+ const states = this.#manager?.states() ?? [];
933
+ return this.#caches.listServiceMessages().filter((message) => {
934
+ if (message.datapoint !== 'STICKY_UNREACH' || message.value === false) {
935
+ return false;
936
+ }
937
+ const type = states.find((state) => state.name === message.interfaceName)?.type ?? '';
938
+ return !isHmipInterface(message.interfaceName, type);
939
+ });
940
+ }
941
+ /**
942
+ * Task 34 (#147, D-42): acknowledges the messages that were in the list when the option was
943
+ * switched on, one after another, each with exactly the write of the acknowledge button
944
+ * (`#acknowledge`, the paced `setValue`). A message that is gone by now - the device came back
945
+ * and somebody acknowledged it, or the edge above got there first - is skipped rather than
946
+ * written twice. A failure is a notice and the write log's red line, as for the edge.
947
+ */
948
+ async #acknowledgeExisting(messages) {
949
+ for (const message of messages) {
950
+ const listed = this.#caches
951
+ .listServiceMessages(message.interfaceName)
952
+ .some((entry) => entry.address === message.address && entry.datapoint === message.datapoint);
953
+ if (!listed || this.#stopped) {
954
+ continue;
955
+ }
956
+ try {
957
+ await this.#acknowledge(message.interfaceName, message.address, message.datapoint);
958
+ }
959
+ catch (error) {
960
+ this.#notice('info', `${message.address}: STICKY_UNREACH could not be acknowledged: ${errorMessage(error)}`, message.interfaceName);
961
+ }
962
+ }
963
+ }
964
+ /**
965
+ * B-53: a callback address set at start (task 38) or inside a container is taken as it is - the
966
+ * Docker host's address is none of the container's, and it is still the right one.
967
+ */
968
+ #keepsConfiguredCallbackIp() {
969
+ return this.#options.inContainer === true || this.#config.callbackPins?.ip === true;
970
+ }
971
+ #network() {
972
+ if (this.#options.network !== undefined) {
973
+ return this.#options.network;
974
+ }
975
+ const injected = this.#options.localAddresses;
976
+ return injected === undefined ? systemNetwork() : staticNetwork(injected);
977
+ }
978
+ /** B-53: `config.callbackAddresses` - for the host being typed, or the configured one. */
979
+ async #callbackAddresses(host) {
980
+ const { connection } = this.#config;
981
+ const target = typeof host === 'string' ? host.trim() : connection.host;
982
+ const info = await describeCallbackAddresses(target, this.#network(), connection.local === true);
983
+ return this.#keepsConfiguredCallbackIp() ? { ...info, keepsConfigured: true } : info;
984
+ }
659
985
  async #discover() {
660
986
  const discover = this.#options.discover ?? ((options) => discoverCcus(options));
661
987
  const found = await discover({ tls: this.#config.connection.tls });
662
988
  this.#config.setDiscovered(found);
663
- this.events.emit('config.changed', this.#config.config);
989
+ this.events.emit('config.changed', this.#withHostFacts(this.#config.config));
664
990
  return found;
665
991
  }
666
992
  async #clearCaches() {
@@ -712,7 +1038,14 @@ export class Backend {
712
1038
  return null;
713
1039
  }
714
1040
  async #setInstallMode(interfaceName, on, options) {
715
- for (const call of installModeCalls(on, options ?? {})) {
1041
+ const effective = { ...options };
1042
+ if (interfaceName === 'HmIP-RF') {
1043
+ // Task 28, measured in the lab: hmipserver's third `setInstallMode` parameter is a String,
1044
+ // and `setInstallMode(true, 30, 1)` got an empty HTTP reply and no install mode at all.
1045
+ // BidCos's mode integer never reaches HmIP, whoever asks for it.
1046
+ delete effective.mode;
1047
+ }
1048
+ for (const call of installModeCalls(on, effective)) {
716
1049
  await this.#write(interfaceName, call.method, call.params);
717
1050
  }
718
1051
  return null;
@@ -723,11 +1056,68 @@ export class Backend {
723
1056
  async #setNames(entries) {
724
1057
  const written = this.#caches.names.set(entries);
725
1058
  this.#caches.saveNames();
726
- await this.#rega?.rename(written);
1059
+ // task 27: with ReGa as the metadata store the provider writes the rename itself, and a
1060
+ // second `Name()` through the name service would be the same script twice - but only for
1061
+ // the objects the provider holds, and only while ReGa answers it; anything else still goes
1062
+ // the way it always did (found by the e2e suite: a rename lost for good is worse than one
1063
+ // script twice)
1064
+ const meta = this.#meta;
1065
+ const held = meta?.kind === 'rega' && meta.state().reachable ? meta.document().objects : undefined;
1066
+ const throughNames = held === undefined
1067
+ ? written
1068
+ : written.filter((entry) => {
1069
+ const ref = meta?.refFor(entry.address);
1070
+ return ref === undefined || !(ref in held);
1071
+ });
1072
+ if (throughNames.length > 0) {
1073
+ await this.#rega?.rename(throughNames);
1074
+ }
1075
+ // D-40: and into the metadata store, which on an openccu-lite box is the box's own. The
1076
+ // local cache is written first either way, so a store that refuses the write still leaves
1077
+ // the name where the user typed it - and the refusal is reported rather than swallowed.
1078
+ await this.#meta?.setNames(written);
727
1079
  const names = this.#caches.names.all();
728
1080
  this.events.emit('names.changed', names);
729
1081
  return names;
730
1082
  }
1083
+ /*
1084
+ * the metadata store (D-40)
1085
+ */
1086
+ /** The state, even before a connection exists - the settings dialog asks for it either way. */
1087
+ #metaState() {
1088
+ return (this.#meta?.state() ?? {
1089
+ provider: this.#config.connection.metaProvider === 'occulite' ||
1090
+ this.#config.connection.metaProvider === 'rega'
1091
+ ? this.#config.connection.metaProvider
1092
+ : 'local',
1093
+ reachable: false,
1094
+ writable: false,
1095
+ revision: 0,
1096
+ objects: 0,
1097
+ });
1098
+ }
1099
+ #metaSnapshot() {
1100
+ const meta = this.#meta;
1101
+ return {
1102
+ state: this.#metaState(),
1103
+ enums: meta?.enums() ?? {},
1104
+ objects: meta?.objects() ?? {},
1105
+ };
1106
+ }
1107
+ /**
1108
+ * The store, or a typed refusal.
1109
+ *
1110
+ * There is no connection without one - `#connect` always builds a provider, the `local` one
1111
+ * when there is no box - so this only fires before the first connect, which is exactly when a
1112
+ * UI should be told "not connected" rather than shown an empty taxonomy.
1113
+ */
1114
+ async #requireMeta() {
1115
+ await this.#metaReady?.catch(() => undefined);
1116
+ if (!this.#meta) {
1117
+ throw configError('the metadata store is not open yet: connect to a system first');
1118
+ }
1119
+ return this.#meta;
1120
+ }
731
1121
  /*
732
1122
  * paramsets
733
1123
  */
@@ -800,27 +1190,95 @@ export class Backend {
800
1190
  #serviceMessages(interfaceName) {
801
1191
  return this.#caches.listServiceMessages(interfaceName);
802
1192
  }
1193
+ /**
1194
+ * Issue #146: what the refresh button of the service-message tab asks for.
1195
+ *
1196
+ * The list itself comes out of the cache, which the events and a five-minute poll keep up to
1197
+ * date; pressing refresh re-read that cache and therefore changed nothing at all - "Refresh
1198
+ * Button ohne Funktion". This makes the round trip the button promises: `getServiceMessages`
1199
+ * on every connected interface that has the method, and the `:0` sweep on HmIP, which is where
1200
+ * its messages come from. Failures are notices, as everywhere else here - an interface that
1201
+ * does not answer must not turn the button into an error dialog.
1202
+ */
1203
+ async #refreshAllServiceMessages(interfaceName) {
1204
+ const names = (this.#manager?.names() ?? []).filter((name) => (interfaceName === undefined || name === interfaceName) && this.#manager?.isConnected(name) === true);
1205
+ for (const name of names) {
1206
+ if (name === 'HmIP-RF') {
1207
+ await this.sweepHmip(name);
1208
+ }
1209
+ else {
1210
+ await this.#refreshServiceMessages(name);
1211
+ }
1212
+ }
1213
+ return this.#serviceMessages(interfaceName);
1214
+ }
803
1215
  /**
804
1216
  * Does it make sense to ask this interface for its service messages?
805
1217
  *
806
1218
  * The built-in answer comes from the core's table: hmipserver has no `getServiceMessages` (the
807
- * HmIP sweep below reads the `:0` channels instead) and the group process behind
808
- * `VirtualDevices` answers it with invalid XML-RPC. A user-defined interface is asked once and
809
- * then remembered for this session - see {@link isMethodUnsupported}.
1219
+ * HmIP sweep below reads the `:0` channels instead), the group process behind `VirtualDevices`
1220
+ * answers it with invalid XML-RPC, and CUxD with a fault and a warning in the CCU's syslog
1221
+ * (B-26, #158). A user-defined interface is judged by its own `system.listMethods` where that
1222
+ * gives a list, and is otherwise asked once and then remembered for this session - see
1223
+ * {@link isMethodUnsupported}.
810
1224
  */
811
- #hasServiceMessages(interfaceName) {
1225
+ async #hasServiceMessages(interfaceName) {
812
1226
  if (this.#noServiceMessages.has(interfaceName)) {
813
1227
  return false;
814
1228
  }
815
- return this.#manager?.resolved(interfaceName)?.serviceMessages !== false;
1229
+ if (this.#manager?.resolved(interfaceName)?.serviceMessages === false) {
1230
+ return false;
1231
+ }
1232
+ // the table is measured for the built-in processes; asking rfd for its method list after
1233
+ // every `init` would buy nothing
1234
+ if (isKnownInterface(interfaceName)) {
1235
+ return true;
1236
+ }
1237
+ const listed = await this.#methodList(interfaceName);
1238
+ return listed === undefined || listed.has('getServiceMessages');
1239
+ }
1240
+ /**
1241
+ * B-26 (#158): the methods a user-defined interface lists in `system.listMethods`, or
1242
+ * `undefined` when it gave no usable list.
1243
+ *
1244
+ * Asked once and shared by concurrent callers; the answer is dropped when the interface
1245
+ * subscribes again. A process that does not have `system.listMethods` either is remembered for
1246
+ * the connection, so it is not asked after every re-`init`; any other failure is forgotten, and
1247
+ * the next caller asks again.
1248
+ */
1249
+ #methodList(interfaceName) {
1250
+ if (this.#noListMethods.has(interfaceName)) {
1251
+ return Promise.resolve(undefined);
1252
+ }
1253
+ const cached = this.#listedMethods.get(interfaceName);
1254
+ if (cached) {
1255
+ return cached;
1256
+ }
1257
+ const pending = this.#read(interfaceName, 'system.listMethods', []).then((answer) => {
1258
+ const names = asStrings(answer);
1259
+ return names.length === 0 ? undefined : new Set(names);
1260
+ }, (error) => {
1261
+ if (isMethodUnsupported(error)) {
1262
+ this.#noListMethods.add(interfaceName);
1263
+ }
1264
+ if (this.#listedMethods.get(interfaceName) === pending) {
1265
+ this.#listedMethods.delete(interfaceName);
1266
+ }
1267
+ return undefined;
1268
+ });
1269
+ this.#listedMethods.set(interfaceName, pending);
1270
+ return pending;
816
1271
  }
817
1272
  /** Reads the BidCos service messages of one interface into the store. */
818
1273
  async #refreshServiceMessages(interfaceName) {
819
- if (!this.#hasServiceMessages(interfaceName)) {
1274
+ if (!(await this.#hasServiceMessages(interfaceName))) {
820
1275
  return;
821
1276
  }
822
1277
  try {
823
1278
  const answer = await this.#read(interfaceName, 'getServiceMessages', []);
1279
+ if (this.#serviceMessageFailures.delete(interfaceName)) {
1280
+ this.#notice('info', `${interfaceName}: getServiceMessages answers again`, interfaceName);
1281
+ }
824
1282
  if (!Array.isArray(answer)) {
825
1283
  return;
826
1284
  }
@@ -847,8 +1305,14 @@ export class Backend {
847
1305
  // of it on hardware (task 17). Remembered, and never asked again this session.
848
1306
  if (isMethodUnsupported(error)) {
849
1307
  this.#noServiceMessages.add(interfaceName);
1308
+ this.#serviceMessageFailures.delete(interfaceName);
1309
+ return;
1310
+ }
1311
+ // B-26 (#158): once per interface until a call succeeds again, not once per poll
1312
+ if (this.#serviceMessageFailures.has(interfaceName)) {
850
1313
  return;
851
1314
  }
1315
+ this.#serviceMessageFailures.add(interfaceName);
852
1316
  this.#notice('info', `${interfaceName}: getServiceMessages failed: ${errorMessage(error)}`, interfaceName);
853
1317
  }
854
1318
  }
@@ -858,7 +1322,7 @@ export class Backend {
858
1322
  return;
859
1323
  }
860
1324
  const timer = setInterval(() => {
861
- void this.pollServiceMessages();
1325
+ void runWithOrigin('background', () => this.pollServiceMessages());
862
1326
  }, interval);
863
1327
  if (typeof timer.unref === 'function') {
864
1328
  timer.unref();
@@ -886,7 +1350,7 @@ export class Backend {
886
1350
  }
887
1351
  const timer = setTimeout(() => {
888
1352
  this.#hmipSweepTimer = undefined;
889
- void this.sweepHmip();
1353
+ void runWithOrigin('background', () => this.sweepHmip());
890
1354
  }, this.#options.hmipSweepDelayMs ?? HMIP_SWEEP_DELAY_MS);
891
1355
  if (typeof timer.unref === 'function') {
892
1356
  timer.unref();
@@ -921,11 +1385,27 @@ export class Backend {
921
1385
  this.events.emit('serviceMessages.changed', this.#caches.listServiceMessages());
922
1386
  }
923
1387
  }
924
- /** Files the RSSI and the service messages of a `getParamset(<device>:0, VALUES)` answer. */
1388
+ /**
1389
+ * Files the RSSI, the service messages and the unreach state of a `getParamset(<device>:0,
1390
+ * VALUES)` answer.
1391
+ *
1392
+ * Task 34 (B-9): the unreach state goes through `#noteUnreach` like the BidCos poll's, so the
1393
+ * Funk tab counts the outages of HmIP devices that no event reported. `UNREACH` is what says
1394
+ * whether the device is away now; `STICKY_UNREACH` is only read where there is no `UNREACH`,
1395
+ * because a standing sticky flag next to `UNREACH: false` would count the same outage again on
1396
+ * every sweep. Nothing is acknowledged from here: a sweep is a read, and HmIP has nothing to
1397
+ * acknowledge (D-42).
1398
+ */
925
1399
  #applyHmipMaintenance(interfaceName, address, values) {
926
1400
  const device = address.split(':')[0] ?? address;
927
1401
  this.#caches.rssi(interfaceName).applyHmipParamset(device, values);
928
- return this.#caches.serviceMessages.applyParamset(interfaceName, address, values);
1402
+ const changed = this.#caches.serviceMessages.applyParamset(interfaceName, address, values);
1403
+ const datapoint = 'UNREACH' in values ? 'UNREACH' : 'STICKY_UNREACH';
1404
+ const value = values[datapoint];
1405
+ if (value !== undefined) {
1406
+ this.#noteUnreach(interfaceName, address, datapoint, value, { acknowledge: false });
1407
+ }
1408
+ return changed;
929
1409
  }
930
1410
  /**
931
1411
  * Acknowledging a service message writes its datapoint. An `ACTION` is confirmed with `true`,
@@ -1048,7 +1528,9 @@ function asDescriptions(value) {
1048
1528
  const descriptions = [];
1049
1529
  for (const entry of value) {
1050
1530
  if (isStruct(entry) && typeof entry['ADDRESS'] === 'string') {
1051
- descriptions.push(entry);
1531
+ // #143: the list fields of a description are made into lists here, once, for every
1532
+ // interface - the group process sends `PARAMSETS` as a string on some boxes.
1533
+ descriptions.push(normaliseDescription(entry));
1052
1534
  }
1053
1535
  }
1054
1536
  return descriptions;
@@ -1060,17 +1542,30 @@ function asLinks(value) {
1060
1542
  const links = [];
1061
1543
  for (const entry of value) {
1062
1544
  if (isStruct(entry) && typeof entry['SENDER'] === 'string' && typeof entry['RECEIVER'] === 'string') {
1063
- links.push(entry);
1545
+ links.push(withLinkText(entry));
1064
1546
  }
1065
1547
  }
1066
1548
  return links;
1067
1549
  }
1068
1550
  function asLink(value, sender, receiver) {
1069
1551
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1070
- return { SENDER: sender, RECEIVER: receiver, ...value };
1552
+ return withLinkText({ SENDER: sender, RECEIVER: receiver, ...value });
1071
1553
  }
1072
1554
  return { SENDER: sender, RECEIVER: receiver };
1073
1555
  }
1556
+ /**
1557
+ * B-23 (#156): a link's `NAME` and `DESCRIPTION` are UTF-8 in rfd - the WebUI's default description
1558
+ * "Standardverknüpfung" and our own `setLinkInfo` alike - and the XML-RPC client reads them as
1559
+ * ISO-8859-1. Repaired here, once, for both reads; a BIN-RPC answer (its library decodes UTF-8) and
1560
+ * a real ISO-8859-1 text pass through unchanged.
1561
+ */
1562
+ function withLinkText(link) {
1563
+ return {
1564
+ ...link,
1565
+ ...(typeof link.NAME === 'string' ? { NAME: repairMisdecodedUtf8(link.NAME) } : {}),
1566
+ ...(typeof link.DESCRIPTION === 'string' ? { DESCRIPTION: repairMisdecodedUtf8(link.DESCRIPTION) } : {}),
1567
+ };
1568
+ }
1074
1569
  function asBidcosInterfaces(value) {
1075
1570
  if (!Array.isArray(value)) {
1076
1571
  return [];
@@ -1110,6 +1605,7 @@ export const API_METHOD_NAMES = [
1110
1605
  'config.set',
1111
1606
  'config.discover',
1112
1607
  'config.clearCaches',
1608
+ 'config.callbackAddresses',
1113
1609
  'interfaces.list',
1114
1610
  'interfaces.reconnect',
1115
1611
  'rega.state',
@@ -1128,6 +1624,21 @@ export const API_METHOD_NAMES = [
1128
1624
  'devices.replaceable',
1129
1625
  'names.get',
1130
1626
  'names.set',
1627
+ 'meta.state',
1628
+ 'meta.get',
1629
+ 'meta.enums',
1630
+ 'meta.objects',
1631
+ 'meta.setMembership',
1632
+ 'meta.assign',
1633
+ 'meta.enum.create',
1634
+ 'meta.enum.update',
1635
+ 'meta.enum.delete',
1636
+ 'meta.node.create',
1637
+ 'meta.node.update',
1638
+ 'meta.node.delete',
1639
+ 'meta.refresh',
1640
+ 'meta.export',
1641
+ 'meta.import',
1131
1642
  'paramset.get',
1132
1643
  'paramset.description',
1133
1644
  'paramset.put',
@@ -1153,14 +1664,15 @@ export const API_METHOD_NAMES = [
1153
1664
  'unreach.list',
1154
1665
  'unreach.reset',
1155
1666
  'serviceMessages.list',
1667
+ 'serviceMessages.refresh',
1156
1668
  'serviceMessages.ack',
1157
1669
  'events.recent',
1158
1670
  'events.clear',
1159
1671
  'rpc.call',
1160
1672
  'rpc.methods',
1161
1673
  'write.cancel',
1162
- 'writeLog.list',
1163
- 'writeLog.clear',
1674
+ 'rpcLog.list',
1675
+ 'rpcLog.clear',
1164
1676
  'data.file',
1165
1677
  'session.info',
1166
1678
  ];