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.
- package/README.md +2 -1
- package/data/easymode-time-selectors.json +285 -0
- package/data/icons/106_hm-lc-sw1-dr.webp +0 -0
- package/data/icons/108_hm-rc-dis-h-x-eu.webp +0 -0
- package/data/icons/109_hm-lc-sw1-pl-ct.webp +0 -0
- package/data/icons/110_hm-es-pmsw1-dr.webp +0 -0
- package/data/icons/112_hmip-wrc2.webp +0 -0
- package/data/icons/113_hmip-psm-ch.webp +0 -0
- package/data/icons/113_hmip-psm-it.webp +0 -0
- package/data/icons/113_hmip-psm-pe.webp +0 -0
- package/data/icons/113_hmip-psm-uk.webp +0 -0
- package/data/icons/113_hmip-psm.webp +0 -0
- package/data/icons/114_hm-lc-dim1t-fm-lf_3.webp +0 -0
- package/data/icons/116_hm-rc-4-3_brc-h3.webp +0 -0
- package/data/icons/118_hmip-swdo.webp +0 -0
- package/data/icons/119_hmip-rc8.webp +0 -0
- package/data/icons/120_hmip-etrv.webp +0 -0
- package/data/icons/121_hmip-wth.webp +0 -0
- package/data/icons/123_oligo.smart.ip.hm.webp +0 -0
- package/data/icons/125_hmip-smi.webp +0 -0
- package/data/icons/130_hmip-srh.webp +0 -0
- package/data/icons/131_hmip-wrc6.webp +0 -0
- package/data/icons/132_hmip-smo.webp +0 -0
- package/data/icons/133_hmip-asir.webp +0 -0
- package/data/icons/134_hmip-fsm.webp +0 -0
- package/data/icons/135_hmip-fsm16.webp +0 -0
- package/data/icons/136_hmip-miob.webp +0 -0
- package/data/icons/137_hmip-fal-c6.webp +0 -0
- package/data/icons/138_hmip-fal-c10.webp +0 -0
- package/data/icons/140_alpha-ip-rgb.webp +0 -0
- package/data/icons/141_alpha-ip-rgba.webp +0 -0
- package/data/icons/144_hmip-wgc.webp +0 -0
- package/data/icons/145_hmip-froll_hmip-fbl.webp +0 -0
- package/data/icons/146_hmip-sth.webp +0 -0
- package/data/icons/147_hmip-sthd.webp +0 -0
- package/data/icons/148_hmip-stho.webp +0 -0
- package/data/icons/149_hmip-sam.webp +0 -0
- package/data/icons/151_hmip-pcbs-bat.webp +0 -0
- package/data/icons/152_hmip-swdo-i.webp +0 -0
- package/data/icons/153_hmip-spi.webp +0 -0
- package/data/icons/154_hmip-spdr.webp +0 -0
- package/data/icons/155_hmip-whs2.webp +0 -0
- package/data/icons/156_hmip-mod-oc8.webp +0 -0
- package/data/icons/158_hmip-etrv-uk.webp +0 -0
- package/data/icons/159_hmip-mod-rc8.webp +0 -0
- package/data/icons/160_hmipw-drs4.webp +0 -0
- package/data/icons/161_hmipw-drs8.webp +0 -0
- package/data/icons/162_hmipw-drap.webp +0 -0
- package/data/icons/163_hmipw-drbl4.webp +0 -0
- package/data/icons/164_hmipw-dri16.webp +0 -0
- package/data/icons/165_hmipw-fio6.webp +0 -0
- package/data/icons/166_hmipw-drd3.webp +0 -0
- package/data/icons/167_hmipw-dri32.webp +0 -0
- package/data/icons/168_hmip-smi55.webp +0 -0
- package/data/icons/169_hmip-swo-pr.webp +0 -0
- package/data/icons/170_hmip-swo-pl.webp +0 -0
- package/data/icons/171_hmip-swo-b.webp +0 -0
- package/data/icons/172_hmip-swd.webp +0 -0
- package/data/icons/173_hmip-bsl.webp +0 -0
- package/data/icons/174_hmip-slo.webp +0 -0
- package/data/icons/175_hmip-group-heating.webp +0 -0
- package/data/icons/177_hmip-dbb.webp +0 -0
- package/data/icons/178_hmip-etrv-b1.webp +0 -0
- package/data/icons/179_hmip-swdm-b2.webp +0 -0
- package/data/icons/180_hmip-etrv-b.webp +0 -0
- package/data/icons/181_hmip-swdm.webp +0 -0
- package/data/icons/182_hmip-fci1.webp +0 -0
- package/data/icons/183_hmip-mod-tm.webp +0 -0
- package/data/icons/184_hmip-pcbs2.webp +0 -0
- package/data/icons/185_hmip-FCI6.webp +0 -0
- package/data/icons/186_hmip-mp3p.webp +0 -0
- package/data/icons/187_hmip-rcb1.webp +0 -0
- package/data/icons/188_hmip-etrv-c.webp +0 -0
- package/data/icons/190_hmip-sci.webp +0 -0
- package/data/icons/191_hmip-mod-ho.webp +0 -0
- package/data/icons/192_hmip-asir-o.webp +0 -0
- package/data/icons/193_hmip-wt.webp +0 -0
- package/data/icons/194_hmip-wrcd.webp +0 -0
- package/data/icons/195_hmipw-smi55.webp +0 -0
- package/data/icons/196_hmip-ASIR-2.webp +0 -0
- package/data/icons/197_hmip-swdo-pl.webp +0 -0
- package/data/icons/198_hmip-falmot-c12.webp +0 -0
- package/data/icons/199_hmip-mio16-pcb.webp +0 -0
- package/data/icons/200_hmip-wth-b.webp +0 -0
- package/data/icons/201_hmip-stv.webp +0 -0
- package/data/icons/202_hmip-wrcr.webp +0 -0
- package/data/icons/203_hmip-fsi16.webp +0 -0
- package/data/icons/204_hmip-drdi3.webp +0 -0
- package/data/icons/205_hmip-drsi4.webp +0 -0
- package/data/icons/206_hmip-drbli4.webp +0 -0
- package/data/icons/207_hmip-srdo.webp +0 -0
- package/data/icons/208_hmip-dsd-pcb.webp +0 -0
- package/data/icons/209_hmip-etrv-b-uk.webp +0 -0
- package/data/icons/210_hmip-ste2-pcb.webp +0 -0
- package/data/icons/211_hmip-drg-dali.webp +0 -0
- package/data/icons/211_hmip-drsi1.webp +0 -0
- package/data/icons/212_hmip-sfd.webp +0 -0
- package/data/icons/212_hmipw-scthd.webp +0 -0
- package/data/icons/213_hmip-smo230.webp +0 -0
- package/data/icons/213_hmip-wua.webp +0 -0
- package/data/icons/214_hmip-dld.webp +0 -0
- package/data/icons/215_hmip-psm-pe-2.webp +0 -0
- package/data/icons/215_hmip-scth230.webp +0 -0
- package/data/icons/216_hmip-etrv-e.webp +0 -0
- package/data/icons/216_hmip-psm-ch-2.webp +0 -0
- package/data/icons/217_hmip-usbsm.webp +0 -0
- package/data/icons/218_hmip-dls.webp +0 -0
- package/data/icons/219_hmip-fwi.webp +0 -0
- package/data/icons/220_hmip-wsc.webp +0 -0
- package/data/icons/221_hmip-sti.webp +0 -0
- package/data/icons/221_hmip-wkp.webp +0 -0
- package/data/icons/222_hmipw-wgd.webp +0 -0
- package/data/icons/223_hmip-mod-wd-vk.webp +0 -0
- package/data/icons/224_hmip-etrv-cl.webp +0 -0
- package/data/icons/225_hmip-rgbw.webp +0 -0
- package/data/icons/226_hmip-esi.webp +0 -0
- package/data/icons/227_hmip-lsc.webp +0 -0
- package/data/icons/228_elv-sh-sw1-bat.webp +0 -0
- package/data/icons/229_elv-sh-bm-s.webp +0 -0
- package/data/icons/230_hmip-etrv-f.webp +0 -0
- package/data/icons/231_elv-sh-cth.webp +0 -0
- package/data/icons/232_elv-sh-taco.webp +0 -0
- package/data/icons/233_hmip-flc.webp +0 -0
- package/data/icons/234_hmip-etrv-3.webp +0 -0
- package/data/icons/235_hmip-psmco.webp +0 -0
- package/data/icons/236_elv-sh-sb8.webp +0 -0
- package/data/icons/237_elv-sh-ctv.webp +0 -0
- package/data/icons/238_elv-sh-cap.webp +0 -0
- package/data/icons/239_hmip-wgs-f.webp +0 -0
- package/data/icons/240_hmip-wgtc-f.webp +0 -0
- package/data/icons/241_hmip-wgt-f.webp +0 -0
- package/data/icons/242_elv-sh-smsi.webp +0 -0
- package/data/icons/243_elv-sh-crc.webp +0 -0
- package/data/icons/244_elv-sh-krco.webp +0 -0
- package/data/icons/245_hmip-falmot-c8.webp +0 -0
- package/data/icons/246_elv-sh-wsm.webp +0 -0
- package/data/icons/247_hmip-hap-2.webp +0 -0
- package/data/icons/248_hmip-dlp.webp +0 -0
- package/data/icons/249_elv-sh-psmci.webp +0 -0
- package/data/icons/250_elv-sh-sps25.webp +0 -0
- package/data/icons/252_elv-sh-pti2.webp +0 -0
- package/data/icons/253_hmip-udi-smi55.webp +0 -0
- package/data/icons/254_hmip-udi-pb2.webp +0 -0
- package/data/icons/256_ELV-SH-SMS2.webp +0 -0
- package/data/icons/257_ELV-SH-FSI.webp +0 -0
- package/data/icons/258_hmip-m-td15.webp +0 -0
- package/data/icons/259_elv-sh-dusi.webp +0 -0
- package/data/icons/260_hmip-wss.webp +0 -0
- package/data/icons/54a_lc-ddc1-pcb.webp +0 -0
- package/data/icons/72_hm-rc-brc-h.webp +0 -0
- package/data/icons/73_hm-atent.webp +0 -0
- package/data/icons/79_hm-lc-dim1pwm-cv.webp +0 -0
- package/data/icons/84_hm-rc-4-3.webp +0 -0
- package/data/icons/85_hm-rc-sec4-3.webp +0 -0
- package/data/icons/86_hm-rc-key4-3.webp +0 -0
- package/data/icons/CCU3-1-50.webp +0 -0
- package/data/icons/CCU3.webp +0 -0
- package/data/icons/hm-coupling-dim.webp +0 -0
- package/data/icons/hm-coupling-group.webp +0 -0
- package/data/icons/hm-coupling-onoff.webp +0 -0
- package/data/icons/hm-coupling-rgb-dim.webp +0 -0
- package/data/icons/hm-coupling-rgb.webp +0 -0
- package/data/icons/hm-coupling-rgbw-dim.webp +0 -0
- package/data/icons/hm-coupling-rgbw.webp +0 -0
- package/data/icons/hm-coupling-white-dim.webp +0 -0
- package/data/icons/hm-coupling-white.webp +0 -0
- package/data/icons/hm-hue_gateway.webp +0 -0
- package/data/icons/hm-lightify_gateway.webp +0 -0
- package/data/icons/hm_resc-win-pcb-sc.webp +0 -0
- package/data/icons/unknown_device.webp +0 -0
- package/data/manifest.json +7 -1
- package/data/master-metadata.json +1997 -33
- package/data/profiles/ACCESS_RECEIVER.json +26 -13
- package/data/profiles/ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER.json +3270 -76
- package/data/profiles/ACTOR_SECURITY.json +90 -10
- package/data/profiles/ACTOR_WINDOW.json +580 -43
- package/data/profiles/ALARMACTUATOR.json +564 -37
- package/data/profiles/ALARM_COND_SWITCH_RECEIVER.json +36 -4
- package/data/profiles/ALARM_SWITCH_VIRTUAL_RECEIVER.json +735 -33
- package/data/profiles/ARMING.json +115 -5
- package/data/profiles/BLIND.json +1038 -49
- package/data/profiles/BLIND_VIRTUAL_RECEIVER.json +4624 -124
- package/data/profiles/CLIMATECONTROL_FLOOR_PUMP_TRANSCEIVER.json +27 -1
- package/data/profiles/DIMMER.json +2233 -71
- package/data/profiles/DIMMER_VIRTUAL_RECEIVER.json +5773 -102
- package/data/profiles/DIMMER_woLongKeyPress.json +117 -4
- package/data/profiles/DOOR_RECEIVER.json +138 -10
- package/data/profiles/DUAL_WHITE_BRIGHTNESS.json +305 -11
- package/data/profiles/DUAL_WHITE_COLOR.json +730 -23
- package/data/profiles/HEATING_KEY_RECEIVER.json +127 -10
- package/data/profiles/HMW_BLIND.json +555 -20
- package/data/profiles/HMW_DIMMER.json +1337 -47
- package/data/profiles/HMW_INPUT_OUTPUT.json +375 -20
- package/data/profiles/HMW_SWITCH.json +375 -20
- package/data/profiles/JALOUSIE.json +1320 -52
- package/data/profiles/KEYMATIC.json +120 -12
- package/data/profiles/REMOTECONTROL_RECEIVER.json +10 -2
- package/data/profiles/RGBW_AUTOMATIC.json +46 -6
- package/data/profiles/RGBW_COLOR.json +14 -4
- package/data/profiles/SERVO_VIRTUAL_RECEIVER.json +117 -3
- package/data/profiles/SHUTTER_VIRTUAL_RECEIVER.json +4263 -128
- package/data/profiles/SIGNAL_CHIME.json +195 -14
- package/data/profiles/SIGNAL_CHIMEM.json +195 -14
- package/data/profiles/SIGNAL_LED.json +195 -14
- package/data/profiles/SIGNAL_LEDM.json +195 -14
- package/data/profiles/SIMPLE_SWITCH_RECEIVER.json +113 -9
- package/data/profiles/STATUS_INDICATOR.json +1073 -45
- package/data/profiles/SWITCH.json +1412 -60
- package/data/profiles/SWITCH_PANIC.json +315 -13
- package/data/profiles/SWITCH_SENSOR.json +243 -11
- package/data/profiles/SWITCH_VIRTUAL_RECEIVER.json +3963 -131
- package/data/profiles/UNIVERSAL_ACTOR.json +4319 -90
- package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_LSC.json +6094 -196
- package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_PWM.json +4920 -101
- package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_RGB(W).json +2353 -80
- package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_RGBW_DALI.json +6116 -176
- package/data/profiles/UNIVERSAL_LIGHT_RECEIVER_TW.json +3844 -104
- package/data/profiles/VIRTUAL_DIMMER.json +305 -11
- package/data/profiles/VIRTUAL_DUAL_WHITE_COLOR.json +730 -23
- package/data/profiles/WATER_SWITCH_VIRTUAL_RECEIVER.json +3903 -128
- package/data/profiles/WINDOW_DRIVE_RECEIVER.json +220 -8
- package/data/profiles/WINMATIC.json +505 -23
- package/dist/cli.js +24 -0
- package/dist/occulite.d.ts +86 -0
- package/dist/occulite.js +140 -0
- package/dist/options.d.ts +37 -6
- package/dist/options.js +49 -6
- package/dist/server.d.ts +47 -4
- package/dist/server.js +237 -27
- package/dist/sessions.d.ts +9 -1
- package/dist/sessions.js +8 -2
- package/dist/testSupport.js +9 -2
- package/node_modules/@homematic-manager/backend/dist/api/backend.d.ts +33 -0
- package/node_modules/@homematic-manager/backend/dist/api/backend.js +558 -46
- package/node_modules/@homematic-manager/backend/dist/cache/names.d.ts +9 -0
- package/node_modules/@homematic-manager/backend/dist/cache/names.js +21 -0
- package/node_modules/@homematic-manager/backend/dist/config/defaults.js +10 -1
- package/node_modules/@homematic-manager/backend/dist/config/store.d.ts +28 -1
- package/node_modules/@homematic-manager/backend/dist/config/store.js +59 -3
- package/node_modules/@homematic-manager/backend/dist/devices/installMode.js +9 -0
- package/node_modules/@homematic-manager/backend/dist/errors.d.ts +18 -0
- package/node_modules/@homematic-manager/backend/dist/errors.js +51 -2
- package/node_modules/@homematic-manager/backend/dist/images/deviceImages.d.ts +6 -0
- package/node_modules/@homematic-manager/backend/dist/images/deviceImages.js +38 -2
- package/node_modules/@homematic-manager/backend/dist/index.d.ts +3 -1
- package/node_modules/@homematic-manager/backend/dist/index.js +4 -1
- package/node_modules/@homematic-manager/backend/dist/interfaces/manager.d.ts +70 -8
- package/node_modules/@homematic-manager/backend/dist/interfaces/manager.js +333 -44
- package/node_modules/@homematic-manager/backend/dist/meta/client.d.ts +120 -0
- package/node_modules/@homematic-manager/backend/dist/meta/client.js +307 -0
- package/node_modules/@homematic-manager/backend/dist/meta/credentials.d.ts +38 -0
- package/node_modules/@homematic-manager/backend/dist/meta/credentials.js +53 -0
- package/node_modules/@homematic-manager/backend/dist/meta/index.d.ts +12 -0
- package/node_modules/@homematic-manager/backend/dist/meta/index.js +12 -0
- package/node_modules/@homematic-manager/backend/dist/meta/localProvider.d.ts +47 -0
- package/node_modules/@homematic-manager/backend/dist/meta/localProvider.js +119 -0
- package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.d.ts +79 -0
- package/node_modules/@homematic-manager/backend/dist/meta/occuliteProvider.js +305 -0
- package/node_modules/@homematic-manager/backend/dist/meta/provider.d.ts +68 -0
- package/node_modules/@homematic-manager/backend/dist/meta/provider.js +19 -0
- package/node_modules/@homematic-manager/backend/dist/meta/regaProvider.d.ts +84 -0
- package/node_modules/@homematic-manager/backend/dist/meta/regaProvider.js +403 -0
- package/node_modules/@homematic-manager/backend/dist/meta/service.d.ts +137 -0
- package/node_modules/@homematic-manager/backend/dist/meta/service.js +412 -0
- package/node_modules/@homematic-manager/backend/dist/rega/client.d.ts +11 -0
- package/node_modules/@homematic-manager/backend/dist/rega/client.js +13 -0
- package/node_modules/@homematic-manager/backend/dist/rega/scripts.d.ts +51 -0
- package/node_modules/@homematic-manager/backend/dist/rega/scripts.js +171 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/client.d.ts +18 -5
- package/node_modules/@homematic-manager/backend/dist/rpc/client.js +11 -5
- package/node_modules/@homematic-manager/backend/dist/rpc/log.d.ts +86 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/log.js +241 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/origin.d.ts +30 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/origin.js +35 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/server.d.ts +12 -0
- package/node_modules/@homematic-manager/backend/dist/rpc/server.js +37 -5
- package/node_modules/@homematic-manager/backend/dist/util/net.d.ts +86 -1
- package/node_modules/@homematic-manager/backend/dist/util/net.js +223 -9
- package/node_modules/@homematic-manager/backend/package.json +1 -1
- package/node_modules/@homematic-manager/core/dist/api/types.d.ts +397 -10
- package/node_modules/@homematic-manager/core/dist/api/types.js +23 -2
- package/node_modules/@homematic-manager/core/dist/data/memory.d.ts +3 -1
- package/node_modules/@homematic-manager/core/dist/data/memory.js +3 -0
- package/node_modules/@homematic-manager/core/dist/data/types.d.ts +60 -0
- package/node_modules/@homematic-manager/core/dist/data/types.js +2 -0
- package/node_modules/@homematic-manager/core/dist/devices/index.d.ts +19 -0
- package/node_modules/@homematic-manager/core/dist/devices/index.js +53 -0
- package/node_modules/@homematic-manager/core/dist/easymodes/engine.d.ts +3 -1
- package/node_modules/@homematic-manager/core/dist/easymodes/engine.js +1 -0
- package/node_modules/@homematic-manager/core/dist/easymodes/form.d.ts +43 -0
- package/node_modules/@homematic-manager/core/dist/easymodes/form.js +84 -0
- package/node_modules/@homematic-manager/core/dist/i18n/messages.js +61 -0
- package/node_modules/@homematic-manager/core/dist/index.d.ts +3 -0
- package/node_modules/@homematic-manager/core/dist/index.js +4 -0
- package/node_modules/@homematic-manager/core/dist/interfaces/table.d.ts +23 -2
- package/node_modules/@homematic-manager/core/dist/interfaces/table.js +18 -1
- package/node_modules/@homematic-manager/core/dist/meta/document.d.ts +71 -0
- package/node_modules/@homematic-manager/core/dist/meta/document.js +260 -0
- package/node_modules/@homematic-manager/core/dist/meta/follow.d.ts +27 -0
- package/node_modules/@homematic-manager/core/dist/meta/follow.js +180 -0
- package/node_modules/@homematic-manager/core/dist/meta/index.d.ts +16 -0
- package/node_modules/@homematic-manager/core/dist/meta/index.js +16 -0
- package/node_modules/@homematic-manager/core/dist/meta/paths.d.ts +96 -0
- package/node_modules/@homematic-manager/core/dist/meta/paths.js +182 -0
- package/node_modules/@homematic-manager/core/dist/meta/slug.d.ts +22 -0
- package/node_modules/@homematic-manager/core/dist/meta/slug.js +59 -0
- package/node_modules/@homematic-manager/core/dist/meta/store.d.ts +147 -0
- package/node_modules/@homematic-manager/core/dist/meta/store.js +537 -0
- package/node_modules/@homematic-manager/core/dist/meta/tree.d.ts +19 -0
- package/node_modules/@homematic-manager/core/dist/meta/tree.js +53 -0
- package/node_modules/@homematic-manager/core/dist/meta/types.d.ts +143 -0
- package/node_modules/@homematic-manager/core/dist/meta/types.js +63 -0
- package/node_modules/@homematic-manager/core/dist/meta/view.d.ts +50 -0
- package/node_modules/@homematic-manager/core/dist/meta/view.js +103 -0
- package/node_modules/@homematic-manager/core/dist/rpc/methods.d.ts +10 -0
- package/node_modules/@homematic-manager/core/dist/rpc/methods.js +51 -1
- package/node_modules/@homematic-manager/core/dist/rpc/text.d.ts +22 -0
- package/node_modules/@homematic-manager/core/dist/rpc/text.js +46 -0
- package/node_modules/@homematic-manager/core/dist/rssi/index.d.ts +125 -13
- package/node_modules/@homematic-manager/core/dist/rssi/index.js +146 -29
- package/node_modules/@homematic-manager/core/dist/serviceMessages/index.d.ts +28 -2
- package/node_modules/@homematic-manager/core/dist/serviceMessages/index.js +44 -6
- package/node_modules/@homematic-manager/core/package.json +1 -1
- package/package.json +3 -3
- package/ui/assets/index-D-NsbBVS.css +1 -0
- package/ui/assets/index-DNUNMBbb.js +16 -0
- package/ui/favicon.ico +0 -0
- package/ui/index.html +3 -2
- package/node_modules/@homematic-manager/backend/dist/write/log.d.ts +0 -51
- package/node_modules/@homematic-manager/backend/dist/write/log.js +0 -151
- package/ui/assets/index-lB-HxsKR.js +0 -13
- 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 {
|
|
31
|
-
import { isWriteMethod } from '../
|
|
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
|
-
#
|
|
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.#
|
|
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('
|
|
133
|
+
this.events.emit('rpcLog.appended', entry);
|
|
90
134
|
},
|
|
91
135
|
onError: (error) => {
|
|
92
|
-
this.#notice('warn', `
|
|
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.#
|
|
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.#
|
|
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
|
-
|
|
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 '
|
|
365
|
-
return this.#
|
|
366
|
-
case '
|
|
367
|
-
this.#
|
|
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
|
-
|
|
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 (
|
|
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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
808
|
-
*
|
|
809
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
1163
|
-
'
|
|
1674
|
+
'rpcLog.list',
|
|
1675
|
+
'rpcLog.clear',
|
|
1164
1676
|
'data.file',
|
|
1165
1677
|
'session.info',
|
|
1166
1678
|
];
|