Compare commits

...

409 Commits

Author SHA1 Message Date
Koushik Dutta
fcf87cc559 postbeta 2024-04-19 20:06:19 -07:00
Koushik Dutta
12c1d02a5b server: fix auto restart bug lol 2024-04-19 20:06:08 -07:00
Koushik Dutta
216504639b postbeta 2024-04-19 19:42:28 -07:00
Koushik Dutta
6eae1c7de3 server: plugin reload/deletion race. 2024-04-19 19:42:13 -07:00
Koushik Dutta
a5a1959bd0 predict: fix blank detections in dropdown 2024-04-19 11:48:44 -07:00
Koushik Dutta
62e23880fd coreml: handle batching hint failures 2024-04-19 10:33:37 -07:00
Koushik Dutta
9e652c3521 videoanalysis: fix model hints 2024-04-19 09:31:27 -07:00
Koushik Dutta
97004577f3 videoanalysis: id hint 2024-04-19 09:06:53 -07:00
Koushik Dutta
6f3eac4e43 predict: add yolov6s 2024-04-19 08:41:27 -07:00
Koushik Dutta
c435e351c7 server: fix potential race condition around plugin restart requests 2024-04-19 07:46:39 -07:00
Koushik Dutta
ffc9ca14b5 predict: fix double periodic restarts 2024-04-19 07:37:40 -07:00
Koushik Dutta
9b349fdadc Merge branch 'main' of github.com:koush/scrypted 2024-04-18 18:39:50 -07:00
Koushik Dutta
7cf0c427f9 detect: yolov6 2024-04-18 18:39:45 -07:00
Techno Tim
2fb4fbab15 fix(iface logic): Added net to the hlper functions to also detect ifaces that start with 'net' (#1437) 2024-04-18 15:35:50 -07:00
Koushik Dutta
4a50095049 Update build-plugins-changed.yml 2024-04-18 09:58:33 -07:00
Koushik Dutta
2510fafcf7 videonanalysis: notes on character casing todo. 2024-04-18 09:56:38 -07:00
Koushik Dutta
47897da6fb videoanalysis: improve similar character checker 2024-04-18 08:41:49 -07:00
Koushik Dutta
94055d032b unifi-protect: document license plate api 2024-04-17 22:36:28 -07:00
Koushik Dutta
3e7535cc42 unifi-protect: add lpr 2024-04-17 22:35:49 -07:00
Koushik Dutta
8d47e9c473 coreml/core: batching support 2024-04-17 11:27:57 -07:00
Koushik Dutta
3897e78bdc coreml/sdk: batching support 2024-04-17 10:59:54 -07:00
Koushik Dutta
2fbc0c2573 videoanalysis: fix settings order 2024-04-17 08:31:23 -07:00
Koushik Dutta
1c8fd2399d videoanalysis: add support for lpr on smart motion sensor 2024-04-17 08:26:53 -07:00
Koushik Dutta
3abb6472a7 postbeta 2024-04-16 23:24:24 -07:00
Koushik Dutta
6a221eee98 videoanalysis: wip license plate smart sensor 2024-04-16 23:24:12 -07:00
Koushik Dutta
ad9e9f2d1d predict: publish betas 2024-04-16 23:24:00 -07:00
Koushik Dutta
8c6afde1fc common: reduce logging 2024-04-16 23:23:40 -07:00
Koushik Dutta
b7a8f97198 server: fix potential race conditions around plugin restart 2024-04-16 23:23:28 -07:00
Koushik Dutta
f5fabfeedf homekit: fix hidden setting 2024-04-16 15:50:43 -07:00
Koushik Dutta
494f881d05 openvino: change default model 2024-04-16 15:49:47 -07:00
Koushik Dutta
7192c5ddc2 openvino: fix potential thread safety.
coreml/openvino: more recognition wip
2024-04-16 15:48:40 -07:00
Koushik Dutta
b4da52eaa2 snapshot: improve caching 2024-04-16 10:26:40 -07:00
Koushik Dutta
584ea97b08 mqtt: Fix autodiscvoery 2024-04-16 10:26:25 -07:00
Koushik Dutta
807ba81d92 homekit: publish 2024-04-15 11:07:51 -07:00
Koushik Dutta
0e35bac42a homekit: addIdentifyingMaterial false 2024-04-15 10:54:54 -07:00
Koushik Dutta
a5a464e000 homekit: undo revert for later publish 2024-04-15 10:53:58 -07:00
Koushik Dutta
a3a878cbd5 homekit: revert for longer staged rollout 2024-04-15 10:52:19 -07:00
Koushik Dutta
8abdab70e9 coreml/openvino: publish 2024-04-15 10:35:10 -07:00
Koushik Dutta
69fd86a684 homekit: handle mp4 generation shutdown 2024-04-15 07:52:25 -07:00
Koushik Dutta
f0e85f14a9 Merge branch 'main' of github.com:koush/scrypted 2024-04-15 07:22:45 -07:00
Koushik Dutta
6130b7fa0c homekit: add identifying material to prevent name clashing 2024-04-15 07:22:41 -07:00
slyoldfox
6dba80c277 Fixes handling the welcome message status from bticino (#1432)
* Fix voicemail status by parsing the VSC more accurately

* Implement video clip streaming by fetching and converting the voicemail message
2024-04-14 09:35:37 -07:00
Koushik Dutta
0f4ff0d4fc sdk: add missing dom types 2024-04-13 20:05:24 -07:00
Koushik Dutta
3d58600c5f postbeta 2024-04-13 12:53:34 -07:00
Koushik Dutta
9c9909e05b amcrest: Fix probe 2024-04-13 12:28:39 -07:00
Koushik Dutta
9c0d253cae webrtc: fix audio handling on unrecognized codec 2024-04-13 09:43:34 -07:00
Koushik Dutta
c1c9fec62f openvino: working lpr and face recog; 2024-04-12 22:12:07 -07:00
Koushik Dutta
27a1c5269a coreml: fixup detection test 2024-04-12 22:04:55 -07:00
Koushik Dutta
c0c938d9c4 snapshot: fix image resize defaults 2024-04-12 21:44:26 -07:00
Koushik Dutta
1dae1834ad predict: missing files 2024-04-12 20:45:31 -07:00
Koushik Dutta
250b2554d7 coreml: dead code 2024-04-12 18:43:11 -07:00
Koushik Dutta
35de80e94a openvino/coreml: refacotr 2024-04-12 18:41:55 -07:00
Koushik Dutta
ba2bf5692f openvino: update 2024-04-12 12:34:57 -07:00
Koushik Dutta
4684ea6592 coreml: recognition fixes 2024-04-12 12:22:37 -07:00
Koushik Dutta
2ab74bc0f8 core: add label support 2024-04-12 08:42:07 -07:00
Koushik Dutta
0a888364b2 coreml: working lpr 2024-04-11 23:53:30 -07:00
Koushik Dutta
c6ea727a0c mqtt: fix switch 2024-04-11 13:14:06 -07:00
Koushik Dutta
96a0a6bd90 snapshots/fetch: fix request teardown? 2024-04-11 12:54:10 -07:00
Koushik Dutta
bf783c7c3c mqtt: switch auto discovery 2024-04-11 10:07:04 -07:00
Koushik Dutta
cbd11908af homekit: fix aac transcoding for silent audio 2024-04-10 11:36:45 -07:00
Koushik Dutta
3367856715 webrtc: add temporary stun fallback 2024-04-10 10:35:45 -07:00
Koushik Dutta
16d38906fe postbeta 2024-04-09 22:19:09 -07:00
Koushik Dutta
fb37f9f58d coreml: remove yolo9c 2024-04-08 15:07:29 -07:00
Koushik Dutta
7514ccf804 detect: remove old models 2024-04-08 14:57:47 -07:00
Koushik Dutta
267a53e84b ha: publish 2024-04-08 11:33:06 -07:00
Koushik Dutta
10a7877522 tensorflow-lite: fix prediction crash 2024-04-08 09:29:52 -07:00
Koushik Dutta
f15526f78d openvino: switch to scrypted_yolov9c_320 2024-04-07 23:20:31 -07:00
Koushik Dutta
524f9122b7 server: update deps 2024-04-07 22:37:13 -07:00
Koushik Dutta
c35142a112 openvino/coreml: new models 2024-04-07 22:37:04 -07:00
Koushik Dutta
ae63e6004e amcrest: add motion pulse 2024-04-07 19:20:24 -07:00
Koushik Dutta
ab90e2ec02 amcrest: add face detection type 2024-04-07 13:30:07 -07:00
Koushik Dutta
96d536f4b2 tensorflow-lite: change default model for usb 2024-04-07 11:55:31 -07:00
Koushik Dutta
c678b31f6f core/sdk: additional scriptign improvements 2024-04-07 10:28:31 -07:00
Koushik Dutta
0315466b0a core: add storage settings to scripts 2024-04-07 10:19:09 -07:00
Koushik Dutta
0db3b7df5a homekit: datamigration for addIdentifyingMaterial 2024-04-06 20:46:58 -07:00
Koushik Dutta
00d8054de8 homekit: add identifying material to prevent mdns collision 2024-04-06 20:30:11 -07:00
Koushik Dutta
3907547c6f webrtc: fix potential crashes 2024-04-06 12:01:55 -07:00
Koushik Dutta
bd3bc0dcb3 coreml: handle empty face set error 2024-04-06 10:52:44 -07:00
Koushik Dutta
b36783df0a coreml: improve face recognition concurrency 2024-04-05 12:32:29 -07:00
Koushik Dutta
b676c27316 docker: initial nvidia support 2024-04-04 12:48:50 -07:00
Koushik Dutta
bcea7b869b coreml: fix threading 2024-04-04 12:48:11 -07:00
Koushik Dutta
2dd549c042 alexa: fix syncedDevices being undefined 2024-04-04 11:39:43 -07:00
Koushik Dutta
c06e3623b6 amcrest: additional dahua hackery 2024-04-03 10:40:03 -07:00
Koushik Dutta
008e0ecbf7 amcrest: Fix buggy htp firmware on some dahua 2024-04-03 09:48:10 -07:00
Koushik Dutta
e6cb41168f snapshot: better error reporting 2024-04-03 08:57:20 -07:00
Koushik Dutta
95ac72c5c8 coreml: encode embedding 2024-04-02 22:07:13 -07:00
Koushik Dutta
faa667f622 sdk: add embedding field 2024-04-02 21:04:09 -07:00
Koushik Dutta
32868c69fe coreml: working face recog 2024-04-02 20:31:40 -07:00
Koushik Dutta
207cb9d833 homekit: clean up late generator bug 2024-04-02 20:31:30 -07:00
Koushik Dutta
f2de58f59a homekit: fix annexb detection 2024-04-02 15:25:32 -07:00
Koushik Dutta
484682257b coreml: wip face recog 2024-04-02 15:08:54 -07:00
Koushik Dutta
b0b922d209 coreml: plug in inception v1 reset face recognition 2024-04-01 21:36:19 -07:00
Koushik Dutta
e37295fb20 coreml: move vision framework into coreml 2024-04-01 20:45:31 -07:00
Koushik Dutta
2e72366d41 vision-framework: initial release 2024-04-01 10:20:49 -07:00
Koushik Dutta
97b09442e8 tensorflow-lite: fix windows 2024-03-31 16:28:34 -07:00
Koushik Dutta
c2defb8c08 onvif: fix two way audio buffer mtu overflow 2024-03-31 13:24:49 -07:00
Koushik Dutta
aa255530aa postrelease 2024-03-30 20:13:03 -07:00
Koushik Dutta
0b26f4df39 snapshot: avoid internal api 2024-03-30 20:06:02 -07:00
Koushik Dutta
be98083557 webrtc: fix ffmpeg leaks? 2024-03-29 23:32:15 -07:00
Koushik Dutta
f4dcb8e662 openvino: publish new models 2024-03-29 23:16:23 -07:00
Koushik Dutta
45186316a6 tensorflow-lite: publish new models 2024-03-29 23:03:12 -07:00
Koushik Dutta
c6e6c881fe coreml: update models. publish. 2024-03-29 22:52:44 -07:00
Koushik Dutta
62b07ea609 core: fix node upgrade 2024-03-29 13:08:41 -07:00
Koushik Dutta
a00ae60ab0 ha/proxmox: bump versions 2024-03-29 12:53:44 -07:00
Brett Jia
878753a526 server: treat self.device as future (#1401)
* server: treat self.device as future

* simplify

* modify annotation

* modify annotation
2024-03-28 19:36:14 -07:00
Koushik Dutta
3c1801ad01 cameras: fix signal + timeout combined usage 2024-03-27 22:05:05 -07:00
Koushik Dutta
30f9e358b7 cameras: fix fetch timeout bugs 2024-03-27 22:02:26 -07:00
Koushik Dutta
456faea1fd amcrest: fix two way audio termination 2024-03-27 15:39:42 -07:00
Koushik Dutta
5e58b1426e amcrest: fix two way audio termination 2024-03-27 15:39:21 -07:00
Koushik Dutta
ec6d617c09 cameras: update onvif two way 2024-03-27 12:21:41 -07:00
Koushik Dutta
1238abedb1 hikvision: fix events crossing streams in camera channels 2024-03-27 10:02:33 -07:00
Koushik Dutta
3e18b9e6aa amcrest: fix vehicle detection class 2024-03-26 19:29:58 -07:00
Koushik Dutta
dce76b5d87 amcrest: fix object detector types 2024-03-26 18:13:16 -07:00
Koushik Dutta
de645dfacb hikvision: add vehicle support 2024-03-26 14:08:34 -07:00
Koushik Dutta
6fd66db896 amcrest/hikvision: add support for smart detections. publish. 2024-03-26 10:41:32 -07:00
Koushik Dutta
62850163d7 hikvision: implement smart events 2024-03-25 23:22:02 -07:00
Koushik Dutta
b46a385a81 onvif: increase 2 way audio buffer to reduce stutter. 2024-03-25 13:28:29 -07:00
Koushik Dutta
c94fb231c6 cli: fix updater 2024-03-25 12:45:10 -07:00
Koushik Dutta
a3df934a88 chromecast: fix audio playback 2024-03-25 12:44:25 -07:00
Koushik Dutta
a6143e103e postrelease 2024-03-25 12:09:11 -07:00
Koushik Dutta
df705cb0e7 server: rollback portable python to 3.10 2024-03-25 12:09:00 -07:00
Koushik Dutta
6e7f291f81 hikvision: fix two way audio duration 2024-03-25 11:02:58 -07:00
Koushik Dutta
fa5b9f66db python-codecs: Fix process exit leak 2024-03-23 17:45:38 -07:00
Koushik Dutta
f760840a6d ha: publish 2024-03-23 12:48:30 -07:00
Koushik Dutta
f36ee6ccb5 postrelease
postrelease

postrelease
2024-03-23 12:34:40 -07:00
Koushik Dutta
bb610f2bb1 python-codecs: gstreamer now optional 2024-03-23 12:33:37 -07:00
Koushik Dutta
6182369804 core: make lxc always restart 2024-03-23 12:21:22 -07:00
Koushik Dutta
70c4d62466 videonanalysis: add filtering options to smart motion sensor 2024-03-22 19:18:04 -07:00
Koushik Dutta
c20c960a4c core: Fix lxc upgrade 2024-03-22 19:08:57 -07:00
Koushik Dutta
da95729299 postbeta 2024-03-22 09:02:04 -07:00
Koushik Dutta
35444f3f1a server: load env from scrypted volume 2024-03-22 09:01:54 -07:00
Koushik Dutta
8dbf751cd9 core: update lxc with avahi support 2024-03-21 22:22:38 -07:00
Koushik Dutta
e9eecd145e postbeta 2024-03-21 22:07:53 -07:00
Koushik Dutta
94350669b1 postbeta 2024-03-21 19:45:15 -07:00
Koushik Dutta
5876fe9ff5 server: update deps 2024-03-21 19:44:57 -07:00
Koushik Dutta
04cd033565 postbeta 2024-03-21 19:27:30 -07:00
Koushik Dutta
1c3bfc5acb server: add flag to force portable 2024-03-21 19:25:02 -07:00
Koushik Dutta
41a09629bf server: fix comments 2024-03-21 14:15:32 -07:00
Koushik Dutta
fa4cf60c21 postbeta 2024-03-21 14:14:56 -07:00
Koushik Dutta
b2848c1496 ha: rollback accidental version change 2024-03-21 14:04:31 -07:00
Koushik Dutta
514483c69c windows: add choco vc redist 2024-03-21 14:02:57 -07:00
Koushik Dutta
6e73f2d95f docker: lite no longer pulls in 300mb of ffmpeg 2024-03-21 13:58:22 -07:00
Koushik Dutta
4535e9f50f docker: fixup build checks 2024-03-21 13:53:34 -07:00
Koushik Dutta
12fc6b1619 docker: fix lite build 2024-03-21 13:46:02 -07:00
Koushik Dutta
f0402564a8 install: upgrade scripts to node 20, provide explicit python paths 2024-03-21 13:41:48 -07:00
Koushik Dutta
86d900a299 postbeta 2024-03-21 13:21:50 -07:00
Koushik Dutta
2cde2b6824 server: add support for versioned python env vars. only use portable python if env is not set. 2024-03-21 13:21:37 -07:00
Koushik Dutta
ff0350abb9 postbeta 2024-03-21 13:04:20 -07:00
Brett Jia
6c6d2ba40e server: bump python runtime (#1389) 2024-03-21 13:02:52 -07:00
Koushik Dutta
857cc656bd client: update 2024-03-20 23:44:13 -07:00
Koushik Dutta
776356fc02 core: lxc avahi check 2024-03-20 20:39:07 -07:00
Koushik Dutta
50d9cee8ea thermostat: remove deprecated 2024-03-20 20:28:31 -07:00
Koushik Dutta
1cb5e43f90 postbeta 2024-03-20 19:19:42 -07:00
Brett Jia
c8df32e6ae server: fix windows color depth detection (#1388) 2024-03-20 17:47:17 -07:00
Koushik Dutta
77c30b4907 core: publish wiht new sdk 2024-03-20 15:24:31 -07:00
Koushik Dutta
96ae2fc89e unifi-protect: Implement privacy masking 2024-03-20 13:22:28 -07:00
Koushik Dutta
a54978e3f0 postbeta 2024-03-20 12:57:24 -07:00
Koushik Dutta
807b9c1950 Merge remote-tracking branch 'origin/main' into beta 2024-03-20 12:57:05 -07:00
Koushik Dutta
be05127147 postbeta 2024-03-20 11:34:36 -07:00
Koushik Dutta
ac1134aa41 server: add support for media object intrinsic conversions 2024-03-20 11:19:56 -07:00
Koushik Dutta
0487c95e00 sdk: update media object 2024-03-20 10:53:07 -07:00
Koushik Dutta
8add1419e9 sdk/server: implement MediaConverter 2024-03-19 22:38:25 -07:00
Koushik Dutta
50d980cc01 server: add support for MediaConverter 2024-03-19 21:25:30 -07:00
Koushik Dutta
3488a3b4ec sdk: MediaConverter 2024-03-19 21:21:11 -07:00
Koushik Dutta
b3abf5af9b videoanalysis: add support for builtin frame generators 2024-03-19 20:35:09 -07:00
Brett Jia
d494f46739 don't clobber global loop policy + propagate exceptions across loops (#1386) 2024-03-19 19:47:34 -07:00
Brett Jia
d3729f3ae7 server: isolate ptpython repl in its own event loop (#1385) 2024-03-19 19:32:35 -07:00
Koushik Dutta
a2fb900166 core: implement lxc udpater 2024-03-19 13:14:39 -07:00
Koushik Dutta
706e37ea68 cli: use nonzero exit to force restart 2024-03-19 13:09:34 -07:00
Koushik Dutta
b7509fbd12 server: restart should trigger npx exit 2024-03-19 12:12:34 -07:00
Koushik Dutta
d994f7c900 webrtc: publush 2024-03-19 12:12:09 -07:00
Koushik Dutta
4e21db52e2 cli: exit after updates 2024-03-19 12:11:51 -07:00
Koushik Dutta
a35fd3b79b reolink: reverse zoom 2024-03-18 16:43:12 -07:00
Koushik Dutta
eebcf1aac5 reolink: implement zoom 2024-03-18 16:34:21 -07:00
Koushik Dutta
704145ce5d postbeta 2024-03-18 14:45:29 -07:00
Koushik Dutta
8f2e15f9df server: use plugin volume for tf install 2024-03-18 14:45:11 -07:00
Koushik Dutta
c5cf8d01ea postbeta 2024-03-18 10:04:58 -07:00
Koushik Dutta
3356777021 server: use fixed python3.11. install custom pythons into module path. 2024-03-18 10:04:47 -07:00
Koushik Dutta
544570d435 webrtc: update werift 2024-03-17 20:13:01 -07:00
Koushik Dutta
e6b9eb6fb5 Merge branch 'beta' of github.com:koush/scrypted into beta 2024-03-17 20:10:56 -07:00
Koushik Dutta
64137c796e webrtc: update werift 2024-03-17 19:48:24 -07:00
Koushik Dutta
3d29478f24 Update install-scrypted-proxmox.sh 2024-03-17 14:17:37 -07:00
Koushik Dutta
862db817db postbeta 2024-03-17 13:30:20 -07:00
Koushik Dutta
7fcc61609e postbeta 2024-03-17 13:26:11 -07:00
Koushik Dutta
4448d82b48 server: fixup python installs 2024-03-17 13:26:00 -07:00
Koushik Dutta
370b63584a postbeta 2024-03-17 13:19:48 -07:00
Koushik Dutta
fda778cdaa postbeta 2024-03-17 13:06:34 -07:00
Koushik Dutta
58d2e14542 server: update deps 2024-03-17 13:06:25 -07:00
Koushik Dutta
577c6a1733 server: lazy install specific python versions 2024-03-17 13:05:01 -07:00
Koushik Dutta
03c4dd5ecc Merge remote-tracking branch 'origin/main' into beta 2024-03-17 10:36:09 -07:00
Koushik Dutta
5b1889e77b webrtc: add connection timeout 2024-03-17 09:42:38 -07:00
Koushik Dutta
d9203318e2 gda: publish 2024-03-17 07:41:22 -07:00
Koushik Dutta
dbce9dac03 gda: fix temperature unit 2024-03-17 07:40:57 -07:00
Koushik Dutta
6b5755cc4d openvino: yolov9 2024-03-16 22:04:02 -07:00
Koushik Dutta
8aee8f39a3 coreml: yolov9c_32 2024-03-16 21:22:21 -07:00
Koushik Dutta
7e9c23b490 sdk: add privacy mask support 2024-03-16 17:06:04 -07:00
Koushik Dutta
a87a88db2a homekit: fix wonky debug subgroup 2024-03-16 15:28:43 -07:00
Koushik Dutta
3af233cd4c homekit: update hap, use connection source address. 2024-03-16 15:23:11 -07:00
Koushik Dutta
67347817fe webrtc: publish 2024-03-16 11:52:50 -07:00
Koushik Dutta
f1121500e1 webrtc: publish 2024-03-16 11:52:30 -07:00
Koushik Dutta
a1cbfe7d26 Revert "webrtc: repacketize h264 (#1260)"
This reverts commit d3dee3a199.
2024-03-16 11:52:12 -07:00
Koushik Dutta
1fa2cae936 webrtc: fix unhandled rejection 2024-03-16 11:51:17 -07:00
Koushik Dutta
7490188986 snapshot: update for sharp, cache authenticated path 2024-03-16 08:15:32 -07:00
Koushik Dutta
374c5364f4 postbeta 2024-03-15 20:06:03 -07:00
Koushik Dutta
719c8af9c4 cli: set lxc ffmpeg path 2024-03-15 12:47:20 -07:00
Koushik Dutta
45c7117cd4 postbeta 2024-03-15 12:15:48 -07:00
Koushik Dutta
ff095a6157 server: switch to @scrypted/ffmpeg-static 2024-03-15 12:15:29 -07:00
Koushik Dutta
a04aa566a2 postbeta 2024-03-14 14:46:26 -07:00
Koushik Dutta
ec8344be7f server: switch to @scrypted/node-pty 2024-03-14 14:46:14 -07:00
Koushik Dutta
e21ac6283b core: use @scrypted/node-pty 2024-03-14 14:33:44 -07:00
Koushik Dutta
a23a73942d rebroadcast: fix content-base handling 2024-03-13 21:16:48 -07:00
Koushik Dutta
e90e9cd2e8 wyze: fix linux detection, python 3.9 2024-03-13 13:14:20 -07:00
Koushik Dutta
8308d5fa46 core: remove dead code 2024-03-13 13:13:35 -07:00
Koushik Dutta
acaebd5c48 server: fix custom runtime pipe 2024-03-13 13:13:18 -07:00
Koushik Dutta
a79bd66969 postbeta 2024-03-12 20:27:11 -07:00
Koushik Dutta
f37b21c0b2 server: plugin loading refacotr 2024-03-12 20:25:26 -07:00
Koushik Dutta
868403ecde core/common: dont crash on script parse failure, add more node types 2024-03-12 20:19:50 -07:00
Koushik Dutta
00aa766a6b coreml: bump coremltools 2024-03-12 20:19:21 -07:00
Koushik Dutta
5bad16859a core: add more node types 2024-03-12 10:35:00 -07:00
Koushik Dutta
ebf7063422 sdk: update 2024-03-12 10:34:35 -07:00
Koushik Dutta
441361e1ec server: plugin init cleanups 2024-03-11 12:56:21 -07:00
Koushik Dutta
3fb519e3b2 sdk: add support for custom runtimes 2024-03-11 12:09:32 -07:00
Koushik Dutta
fd67756ec6 sdk: add context to notification on triggering event, if any 2024-03-11 09:57:00 -07:00
Koushik Dutta
1f7625ca60 sdk/client: update 2024-03-10 19:34:50 -07:00
Koushik Dutta
5640a55507 external: remove face-api 2024-03-10 12:19:13 -07:00
Koushik Dutta
432eb8367e external: remove ffmpeg 2024-03-10 12:18:53 -07:00
Koushik Dutta
59d2657002 server: remove legacy shell endpoint 2024-03-09 17:24:15 -08:00
Koushik Dutta
9012eb9192 core: publish 2024-03-09 17:20:56 -08:00
Koushik Dutta
2918c8fd21 core: conditional pty buttons 2024-03-09 16:47:29 -08:00
Koushik Dutta
90c8e90af7 core: refactor console on top of pty 2024-03-09 16:44:33 -08:00
Koushik Dutta
b83b5196da core: remove legacy repl endpoint 2024-03-09 15:17:41 -08:00
Koushik Dutta
239124cbdc core: move repl to StreamService 2024-03-09 15:15:41 -08:00
Koushik Dutta
f6d931a1eb postbeta 2024-03-09 13:10:05 -08:00
Koushik Dutta
8e37623695 postbeta 2024-03-09 07:43:33 -08:00
Koushik Dutta
2f2c6545a4 server: move pty/wheel back into runtime install 2024-03-09 07:43:23 -08:00
Koushik Dutta
f8669ea693 postbeta 2024-03-09 00:51:14 -08:00
Koushik Dutta
1cb9985cf8 postbeta 2024-03-08 21:15:27 -08:00
Koushik Dutta
3e3e6504bf postbeta 2024-03-08 19:59:01 -08:00
Koushik Dutta
4856193e35 postbeta 2024-03-08 19:50:46 -08:00
Koushik Dutta
28166a1abc postbeta 2024-03-08 19:44:28 -08:00
Koushik Dutta
97de3c7bf6 postbeta 2024-03-08 18:17:10 -08:00
Koushik Dutta
8e75979f07 postbeta 2024-03-08 18:04:46 -08:00
Koushik Dutta
4c8eb9639f server: use separate python version for pip checks 2024-03-08 18:04:35 -08:00
Koushik Dutta
7a0d070c04 postbeta 2024-03-08 17:54:44 -08:00
Koushik Dutta
3052b954bf postbeta 2024-03-08 17:21:45 -08:00
Koushik Dutta
5f715669ee server: remove shim 2024-03-08 17:21:33 -08:00
Koushik Dutta
0de1c6bdd5 postbeta 2024-03-08 17:17:18 -08:00
Koushik Dutta
02f69c3077 postbeta 2024-03-08 17:16:17 -08:00
Koushik Dutta
af5d83ecc0 server: move postinstall into non-ignored path 2024-03-08 17:16:04 -08:00
Koushik Dutta
2143b4e2c2 postbeta 2024-03-08 12:29:53 -08:00
Koushik Dutta
86d38b5081 server: shim portable python ssl ca 2024-03-08 12:28:02 -08:00
Koushik Dutta
a61be80b24 server: postinstall python deps 2024-03-08 11:38:51 -08:00
Koushik Dutta
97e31ec51d server: use target rather than prefix 2024-03-08 09:36:36 -08:00
Koushik Dutta
dd1efe0756 unifi-protect: publish 2024-03-08 08:17:03 -08:00
Koushik Dutta
155cc89239 postbeta 2024-03-07 15:54:43 -08:00
Koushik Dutta
76cdbc6e96 Update bug_report.md 2024-03-07 10:47:46 -08:00
Koushik Dutta
c68a0286e8 Merge branch 'main' into beta 2024-03-07 10:43:15 -08:00
Koushik Dutta
90a7e44704 homekit: use avahi if available. 2024-03-07 10:43:02 -08:00
Koushik Dutta
35031427b2 docker: auto configure avahi 2024-03-07 10:37:34 -08:00
Koushik Dutta
954c7789ba docker: avahi daemon installer/support 2024-03-07 10:37:30 -08:00
Koushik Dutta
b9c4e1cd16 docker: improve avahi docs 2024-03-07 10:37:24 -08:00
Koushik Dutta
cd7e60781c docker: auto configure avahi 2024-03-07 10:28:12 -08:00
Koushik Dutta
4ae2de0467 docker: avahi daemon installer/support 2024-03-07 10:21:36 -08:00
Koushik Dutta
2fdf58db31 docker: improve avahi docs 2024-03-07 10:07:54 -08:00
Koushik Dutta
27af54e929 server/core: use new pty 2024-03-07 08:16:19 -08:00
Koushik Dutta
b7de4d92cf postbeta 2024-03-06 20:59:19 -08:00
Koushik Dutta
82544d2c1b server: switch to @homebridge/node-pty-prebuilt-multiarch 2024-03-06 20:59:07 -08:00
Koushik Dutta
61c32571d8 python-codecs: publish beta 2024-03-06 20:42:14 -08:00
Koushik Dutta
da8032f922 docker: use edgetpu std 2024-03-06 20:26:51 -08:00
Koushik Dutta
e016011f5a docker: add edgetpu and intel to lite? 2024-03-06 20:21:55 -08:00
Koushik Dutta
d8332898f7 docker: edgetpu max 2024-03-06 20:21:20 -08:00
Koushik Dutta
6903d56570 docker: remove python from lite 2024-03-06 20:17:27 -08:00
Koushik Dutta
0fa8a728f7 postbeta 2024-03-06 19:56:33 -08:00
Koushik Dutta
7081cd6605 server: fixup requirements 2024-03-06 19:55:33 -08:00
Koushik Dutta
83f24ebdaa server: use portable python, shim in debugpy 2024-03-06 19:54:57 -08:00
Koushik Dutta
958442b1bd google-device-access: cleanup 2024-03-06 19:07:12 -08:00
Koushik Dutta
b320fd425b cloud/etc: shuffle cors 2024-03-06 17:57:59 -08:00
Koushik Dutta
0e1305ec5e cloud: remove legacy param 2024-03-06 17:53:17 -08:00
Koushik Dutta
1c3c75db33 onvif/unifi: two way audio quality 2024-03-06 13:05:40 -08:00
Koushik Dutta
afd4927e5b github: remove references to dead thin builds 2024-03-06 12:25:22 -08:00
Koushik Dutta
1b647c902f postbeta 2024-03-06 12:23:30 -08:00
Koushik Dutta
45af364215 github: make node 20 default 2024-03-06 12:22:47 -08:00
Koushik Dutta
c5f33f8eb5 server/python-codecs: add support for optional requirements 2024-03-06 11:11:45 -08:00
Koushik Dutta
cb7ea1c624 unifi-protect: fix online spam 2024-03-06 09:27:34 -08:00
Koushik Dutta
e1571e62d3 postbeta 2024-03-06 08:32:17 -08:00
Koushik Dutta
3065ffef94 unifi-protect: missing file 2024-03-06 08:31:41 -08:00
Brett Jia
9c0a59a75a server: graceful repl exit + multi-repl support (#1362)
* server: graceful repl exit + hacky multi-repl support

* remove prints

* more multi-repl layout fixes, ignore benign CancelledError

* add missing import

* disable input function for safety
2024-03-06 07:55:28 -08:00
Koushik Dutta
e75c183511 postbeta 2024-03-05 19:58:59 -08:00
Koushik Dutta
e50c730c9f postbeta 2024-03-05 18:04:43 -08:00
Koushik Dutta
2da94cdc97 unifi-protect: debounce motion sensors, attempt to stabilize nativeids 2024-03-05 18:04:29 -08:00
Koushik Dutta
b4293e3363 server: cleanup python repl 2024-03-05 18:04:04 -08:00
Brett Jia
71ce995276 server: add Python REPL support + introduce optional requirements.txt (#1360)
* wip python repl

* reimplement with ptpython repl

* hide extra prompts, general cleanup

* add ptpython to dependencies

* Revert "add ptpython to dependencies"

This reverts commit 1b476e665b.

* inject system dependencies into requirements for run-time install

* write correct requirements.txt contents to disk

* Revert "write correct requirements.txt contents to disk"

This reverts commit 0ba7f0d91d.

* refactor to introduce optional system deps
2024-03-05 17:39:35 -08:00
Koushik Dutta
54d73f6692 google-home: fix duplicate doorbell. 2024-03-05 13:18:17 -08:00
Koushik Dutta
6ebab812b4 tensorflow-lite: switch usb edgetpu model 2024-03-05 12:55:24 -08:00
Koushik Dutta
9d921544ab postbeta 2024-03-05 10:19:31 -08:00
Koushik Dutta
93c371841c github: build node 20 2024-03-05 09:58:16 -08:00
Koushik Dutta
a73f421cee github: build node 20 2024-03-05 09:56:52 -08:00
Koushik Dutta
090362b0ce server: update deps, publish beta 2024-03-05 09:54:23 -08:00
Koushik Dutta
73607fd1aa snapshot: fix periodic snapshot timeout regression 2024-03-05 08:57:14 -08:00
Koushik Dutta
09c8c114f7 homekit: publish; 2024-03-05 08:50:10 -08:00
Koushik Dutta
dae40ba862 Merge branch 'main' of github.com:koush/scrypted 2024-03-05 08:49:00 -08:00
Koushik Dutta
fd48eee7b2 cameras: add default request timeouts on snapshots 2024-03-05 08:47:49 -08:00
Koushik Dutta
ec19410e0c snapshot: enforce a 5s timeout for web requests 2024-03-05 08:44:50 -08:00
Koushik Dutta
4e5f6885a9 homekit: Update README.md 2024-03-04 12:17:51 -08:00
Koushik Dutta
f41a6383ae homekit: Update README.md 2024-03-04 12:16:50 -08:00
Brett Jia
644f7d3304 homekit: merge child device only if child has homekit enabled (#1343) 2024-03-03 09:46:13 -08:00
ruby~
e2067c156b amcrest: Add support for Dahua VTO locks to Amcrest plugin (#1356) 2024-03-03 09:45:08 -08:00
Koushik Dutta
d325df083f install: use latest node 18 on windows 2024-03-02 18:23:48 -08:00
Koushik Dutta
04de63ae8e python-codecs: weight the image reader to prefer snapshot plugin. tf/ov publish log spam betas. 2024-03-02 10:06:26 -08:00
Koushik Dutta
803a2d7c51 snapshot: rebuild with fixed sdk 2024-03-02 08:27:04 -08:00
Koushik Dutta
a719026e01 mqtt: fix build 2024-03-01 20:47:30 -08:00
Koushik Dutta
1ac5b992d6 webhook: fix build 2024-03-01 20:46:43 -08:00
Koushik Dutta
6766b35438 hikvision: fix two way audio 2024-03-01 17:53:48 -08:00
Koushik Dutta
548ff489c4 client: send cached login info to cloud request as well 2024-03-01 14:08:03 -08:00
Brett Jia
cb742ab75e common: fix behavior of multiple createLocalDescription calls on firefox (#1078)
* use localDescription property

* debug

* debug

* debug

* cleanup
2024-03-01 12:57:31 -08:00
Koushik Dutta
625c1d4e57 google-home: support streaming to the android app! 2024-02-29 10:30:57 -08:00
Koushik Dutta
88604bcdcb cloud: better readme 2024-02-28 10:24:56 -08:00
Koushik Dutta
441cd0d169 cloud: add option to disable cloud registration completely to suppress warnings. 2024-02-28 09:50:46 -08:00
Koushik Dutta
2f1c45f9bd cloud: fix race conditions and error reporting 2024-02-28 09:30:12 -08:00
Koushik Dutta
c3bb9c96de snapshot: publish 2024-02-27 10:00:22 -08:00
Koushik Dutta
954be25d0c rebroadcast/webrtc: republish with sdk fix 2024-02-27 09:59:14 -08:00
Koushik Dutta
0b28454048 sdk: fix regression 2024-02-27 09:57:26 -08:00
Koushik Dutta
349c41657a webrtc/rebroadcast: fix sdp audio detection defaults 2024-02-26 20:11:23 -08:00
Koushik Dutta
acc7f0c4db cloud: support dev env var 2024-02-26 14:53:36 -08:00
Koushik Dutta
36ee539f0c Merge branch 'main' of github.com:koush/scrypted 2024-02-26 13:51:14 -08:00
Koushik Dutta
628c084764 cloud: prepare for multi server 2024-02-26 13:46:58 -08:00
Koushik Dutta
c541aa8b3b Update bug_report.md 2024-02-25 14:57:41 -08:00
Brett Jia
0dc719ca0d core: publish (#1339) 2024-02-24 19:11:27 -08:00
Brett Jia
ead2c5e76f workflows: test build ui with core (#1340)
* workflows: test build ui with core

* trigger dummy change

* Revert "trigger dummy change"

This reverts commit 622601062c.
2024-02-24 19:11:14 -08:00
Koushik Dutta
dbb314b4eb webrtc/common/rebroadcast: fix sdp parsing 2024-02-24 12:02:12 -08:00
Koushik Dutta
a05bcd6ce4 cloud: send server id and friendly name 2024-02-22 18:42:06 -08:00
Koushik Dutta
3a6b244a4a cloud: send server id for multiple servers. update upnp. 2024-02-21 21:21:04 -08:00
Koushik Dutta
d6b9900db5 various: fix sdp parsing issue around codec defaults 2024-02-21 10:02:01 -08:00
Koushik Dutta
8fa5e23797 alexa/google-home: fix potential vulnerability. do not allow local network control using cloud tokens belonging to a different user. the plugins are now locked to a specific scrypted cloud account once paired. 2024-02-21 10:01:03 -08:00
Koushik Dutta
41d042b5bd Merge branch 'main' of github.com:koush/scrypted 2024-02-20 21:42:44 -08:00
Koushik Dutta
81b235c548 alexa/google-home: additional auth token checks to harden endpoints for cloud sharing 2024-02-20 21:42:40 -08:00
Long Zheng
657921a5b3 sdk/server: Fix type of canMixin (#1333)
* Fix type of canMixin

* Allow undefined

* Add void
2024-02-20 13:31:22 -08:00
Long Zheng
a47f7e2566 sdk: define this type (#1332)
* sdk: define this type

* Fix indent
2024-02-19 08:17:05 -08:00
Long Zheng
eec6291d9e CI build changed plugins (#1323)
* Fix WritableDeviceState

* Fix tsconfig error

* Fix test

* Create build-plugins-changed.yml

* Update build-sdk.yml

* Update build-plugins-changed.yml
2024-02-18 23:12:39 -08:00
Brett Jia
064da326c0 core: reset base date on each reschedule call (#1331) 2024-02-18 19:18:57 -08:00
Koushik Dutta
cbf95e1186 homekit: reorder settins, restart rather than prompt. publish. 2024-02-18 17:38:19 -08:00
Koushik Dutta
70aa5b75bf typescript: update sample 2024-02-18 15:52:25 -08:00
Koushik Dutta
dfe34947cb videoanalysis: publish 2024-02-18 15:52:20 -08:00
Koushik Dutta
f7c0091b7c reolink: fix doorbell onvif detections 2024-02-18 15:52:13 -08:00
Koushik Dutta
3dd6f114d4 videoanalysis: beta 2024-02-18 09:53:04 -08:00
Koushik Dutta
a0a8e25e18 sdk: publish 2024-02-18 09:52:48 -08:00
Koushik Dutta
32f0d675bc videoanalysis: nre check release 2024-02-18 09:46:51 -08:00
Koushik Dutta
1306eda422 sdk: fix up revert regression 2024-02-18 09:46:26 -08:00
Koushik Dutta
79f4c27bed videoanalysis: more cpu throttling fixes 2024-02-18 08:45:19 -08:00
Koushik Dutta
eb57698c8b videoanalysis: update 2024-02-18 08:42:46 -08:00
Koushik Dutta
454d96c5d3 Merge branch 'main' of github.com:koush/scrypted 2024-02-17 21:49:54 -08:00
Koushik Dutta
85daf72d66 docker-mdns: remove prototype 2024-02-17 21:49:50 -08:00
Long Zheng
9d50ba79f7 snapshot: black background (#1324)
* Fix WritableDeviceState

* Fix tsconfig error

* Generate black background instead of using black.jpg

Remove redundant blur
2024-02-16 18:16:07 -08:00
Koushik Dutta
764fbbb21b Update bug_report.md 2024-02-16 10:38:34 -08:00
Koushik Dutta
89e9cf343d Update bug_report.md 2024-02-16 10:38:11 -08:00
Long Zheng
dd7d920480 sdk: Add strict types to sdk (#1308)
* Enable strict mode

* Add @types/node

Remove @types/rimraf

* Fix `include` path to be actual `src`

* Add strict to `sdk`

* Assert `getItem`

* Fix types in SDK

* Refactor SDK function to be type safe

* parseValue handle value null or undefined

* Fix types tsconfig

* Make getDeviceConsole required

* Add build-sdk workflow

* Set working directory

* Assert not undefined

* Remove optionals

* Undo addScryptedInterfaceProperties, revert to self executing function

* Use different type

* Make _deviceState private and add ts-ignore

* Remove unused function

* Remove non-null asserts

* Add tsconfig for sdk/types/src

* Get property isOptional from schema

Use typedoc types

* Type fixes

* Fix type

* Fix type

* Revert change
2024-02-15 15:17:31 -08:00
Koushik Dutta
426454f28f sdk: rebuikd 2024-02-15 14:51:01 -08:00
Koushik Dutta
66441ee177 webrtc: repacketize input 2024-02-15 14:50:52 -08:00
Brett Jia
d3dee3a199 webrtc: repacketize h264 (#1260)
* webrtc: repacketize h264 on nalu type 7

* always repacketize

* lower packet size to avoid uint16 overflows

* remove nalu logging

* Revert "remove nalu logging"

This reverts commit e6b6540696.
2024-02-15 11:27:09 -08:00
Koushik Dutta
b174fbc19b Merge branch 'main' of github.com:koush/scrypted 2024-02-15 09:29:44 -08:00
Koushik Dutta
88da7fc5b4 common: add polygon area 2024-02-15 09:29:40 -08:00
Nick Berardi
c8b799f857 mqtt: Added support for ColorSettingTemperature and ColorSettingHsv to the MQTT support. (#1317) 2024-02-15 08:43:11 -08:00
Koushik Dutta
b28eef9d10 unifi-protect: squelch logging 2024-02-15 08:39:31 -08:00
Long Zheng
f66d39f8d9 sdk: calculate required/optional fields from schema (#1321)
* Add tsconfig for sdk/types/src

* Get property isOptional from schema

Use typedoc types
2024-02-15 07:20:56 -08:00
Koushik Dutta
b6cbc126d6 Merge branch 'main' of github.com:koush/scrypted 2024-02-14 15:43:25 -08:00
Koushik Dutta
bee77e121e sdk/server: make various properties non-optional 2024-02-14 15:43:21 -08:00
Nick Berardi
a62f402982 alexa: removed unneeded packages (#1319) 2024-02-14 14:14:29 -08:00
Koushik Dutta
6c67ac6570 Merge branch 'main' of github.com:koush/scrypted 2024-02-14 12:29:06 -08:00
Koushik Dutta
abea872714 amcrest/hikvision: standardize 20s motion timeout 2024-02-14 12:29:01 -08:00
Koushik Dutta
22018ee573 docker: update fstab to nofail 2024-02-14 12:11:41 -08:00
Koushik Dutta
640b2d806d sdk: fix some signatures 2024-02-14 11:21:26 -08:00
Koushik Dutta
f317c8d9ee server/sdk: additional signature fixes 2024-02-14 08:33:31 -08:00
Koushik Dutta
7d3d7be1cd Merge branch 'main' of github.com:koush/scrypted 2024-02-14 08:25:22 -08:00
Koushik Dutta
1ec954ac98 sdk: fix method signatures 2024-02-14 08:25:18 -08:00
Koushik Dutta
7b2ce12f13 videoanalysis: fix min start 2024-02-13 18:00:25 -08:00
Koushik Dutta
3da2e48cf3 Merge branch 'main' of github.com:koush/scrypted 2024-02-13 15:59:14 -08:00
Koushik Dutta
50fcb6aeab betas 2024-02-13 15:59:08 -08:00
Nick Berardi
952b90fc98 alexa: added support for light, outlet, and fan device types (#1318) 2024-02-13 14:48:19 -08:00
Koushik Dutta
0ff95581b1 videoanalysis: add score filter to smart motion sensor 2024-02-13 13:18:09 -08:00
Koushik Dutta
2e02e7f4ef ring: phase out sip streaming 2024-02-13 09:33:22 -08:00
Koushik Dutta
fcc51418c3 videoanalysis: account for cpu throttling 2024-02-13 09:29:36 -08:00
Koushik Dutta
5c31e75f3d Update bug_report.md 2024-02-13 08:38:18 -08:00
Koushik Dutta
f590675198 Update bug_report.md 2024-02-13 06:29:40 -08:00
Rosemary Orchard
23ba720d4f various: Fix typos (#1311) 2024-02-12 15:52:11 -08:00
Koushik Dutta
a64f3e8082 Revert "sdk: Add tsconfig strict to sdk/types (#1306)" (#1307)
This reverts commit a8eb1a21d7.
2024-02-10 16:34:13 -08:00
Long Zheng
a8eb1a21d7 sdk: Add tsconfig strict to sdk/types (#1306)
* Enable strict mode

* Add @types/node

Remove @types/rimraf

* Fix `include` path to be actual `src`
2024-02-10 16:31:20 -08:00
Long Zheng
aa6cd770f8 Fix debugConsole error (#1305) 2024-02-10 12:25:59 -08:00
Koushik Dutta
a06e0d9138 Merge branch 'main' of github.com:koush/scrypted 2024-02-09 20:24:46 -08:00
Koushik Dutta
807a894eac cloud: get reverse proxy hint from callback message 2024-02-09 20:24:41 -08:00
Long Zheng
b1f216b671 homekit/snapshot: Periodic snapshot timeout (#1295)
* Add setting for periodic snapshot timeout

* Add debug logging for snapshot duration

* Revert "Add setting for periodic snapshot timeout"

This reverts commit 305e8817fd.

* Add periodicTimeout to RequestPictureOptions

HomeKit wait up to 2 seconds

* Add logging if fallback

* Rename to timeout

* Update HomeKit snapshot

* Change to log
2024-02-08 09:22:19 -08:00
Brett Jia
7d25053b5a webhook: bump 0.0.25 (#1303) 2024-02-07 22:21:47 -08:00
Koushik Dutta
3fe020c443 cloud: remove axios 2024-02-07 21:47:11 -08:00
Koushik Dutta
16812680d8 Merge branch 'main' of github.com:koush/scrypted 2024-02-07 18:36:02 -08:00
Koushik Dutta
36b36081eb cloud: use cloudflare tunnel for short lived urls 2024-02-07 18:35:57 -08:00
Brett Jia
baf65a0d33 webhook: minor consistency fixes (#1301)
* webhook: minor consistency fixes

* remove string conversion, don't delete if mixin is different

* bump 0.0.23 beta

* revert getMixin

* bump 0.0.24 beta
2024-02-07 12:10:08 -08:00
Koushik Dutta
21ab560671 install: fix disk setup script user account 2024-02-06 20:38:41 -08:00
Koushik Dutta
370401f034 install: remove old scrypted fstab when passing directory 2024-02-06 19:44:50 -08:00
Koushik Dutta
911e56f6fc Merge branch 'main' of github.com:koush/scrypted 2024-02-06 19:27:37 -08:00
Koushik Dutta
2cc229d39c install: remove old scrypted fstab when passing directory 2024-02-06 19:27:34 -08:00
Brett Jia
efd9afd1ea add bash shebang (#1300) 2024-02-05 20:54:48 -08:00
Koushik Dutta
3c49b87b44 snapshot: fix size to fit height 2024-02-05 20:19:15 -08:00
Koushik Dutta
dc5bbc375b homekit: support ipv6 only binding/streaming 2024-02-03 14:55:52 -08:00
Koushik Dutta
962ceb549e webrtc: fix assumption that pcm codecs are supported 2024-02-02 12:29:10 -08:00
Koushik Dutta
3e47855bc6 ha: publsih 2024-01-31 21:41:52 -08:00
Koushik Dutta
6996027626 videoanalysis: fix readme 2024-01-31 14:13:36 -08:00
Koushik Dutta
932b84d1e5 postrelease 2024-01-31 12:10:37 -08:00
Koushik Dutta
801bd46730 server: default auth should be last available option 2024-01-31 12:10:25 -08:00
Koushik Dutta
e5a764d82f reolink: use onvif events on doorbell 2024-01-31 12:10:08 -08:00
Koushik Dutta
7c9cd9f112 postrelease 2024-01-30 10:03:47 -08:00
304 changed files with 10293 additions and 11411 deletions

View File

@@ -7,12 +7,21 @@ assignees: ''
---
# Github Issues is not a Forum
# Github Issues are not a Support or Discussion Forum
**This issue tracker is not for hardware support or feature requests**. If you are troubleshooting adding a device for the first time, use Discord, Reddit, or Github Discussions. However, if something **was working**, and is now **no longer working**, you may create a Github issue.
Before opening an issue, view the device's Console logs in the Scrypted Management web interface.
**DO NOT OPEN ISSUES FOR ANY OF THE FOLLOWING:**
* Server setup assistance. Use Discord, Reddit, or Github Discussions.
* Hardware setup assistance. Use Discord, Reddit, or Github Discussions.
* Feature Requests. Use Discord, Reddit, or Github Discussions.
* Packet loss in your camera logs. This is wifi/network congestion.
* HomeKit weirdness. See HomeKit troubleshooting guide.
However, if something **was working**, and is now **no longer working**, you may create a Github issue.
Created issues that do not meet these requirements or are improperly filled out will be immediately closed.
# New Issue Instructions
1. Delete this section and everything above it.
@@ -34,16 +43,15 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Server (please complete the following information):**
- OS: [e.g. Ubuntu]
- Installation Method: [e.g. Desktop App, Docker, Local]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Hardware Model (please complete the following information):**
- Device: [e.g. Amcrest]
**Client (please complete the following information, if applicable):**
- Software: [e.g. Home app, NVR app, Alexa, Browser]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,51 @@
name: Build changed plugins
on:
# push:
# branches: ["main"]
# paths: ["plugins/**"]
# pull_request:
# paths: ["plugins/**"]
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: 18
- name: Set up base packages
run: ./npm-install.sh
- name: Build changed plugins
run: |
# Get the list of changed directories in /plugins
changed_dirs=$(git diff --name-only HEAD^ HEAD ./plugins | awk -F/ '{print $2}' | uniq)
# Loop through each changed directory
for dir in $changed_dirs; do
pushd "./plugins/$dir"
if [[ "$dir" == "core" ]]; then
# core plugin requires ui to be built
pushd "./ui"
echo "plugins/$dir/ui > npm install"
npm install
echo "plugins/$dir/ui > npm run build"
npm run build
popd
fi
echo "plugins/$dir > npm install"
npm install
echo "plugins/$dir > npm run build"
npm run build
popd
done

25
.github/workflows/build-sdk.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Build SDK
on:
push:
branches: ["main"]
paths: ["sdk/**"]
pull_request:
paths: ["sdk/**"]
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./sdk
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm ci
- run: npm run build

View File

@@ -11,8 +11,8 @@ jobs:
strategy:
matrix:
NODE_VERSION: [
"18",
# "20"
# "18",
"20"
]
BASE: ["jammy"]
FLAVOR: ["full", "lite"]

View File

@@ -20,12 +20,8 @@ jobs:
strategy:
matrix:
BASE: [
"18-jammy-full",
"18-jammy-lite",
# "18-jammy-thin",
# "20-jammy-full",
# "20-jammy-lite",
# "20-jammy-thin",
"20-jammy-full",
"20-jammy-lite",
]
SUPERVISOR: ["", ".s6"]
steps:
@@ -85,19 +81,15 @@ jobs:
push: true
tags: |
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
${{ matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:thin-s6' || '' }}
${{ matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.publish_tag || steps.package-version.outputs.NPM_VERSION) }}
${{ matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '18-jammy-thin' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:thin-s6' || '' }}
${{ matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.tag) || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
${{ github.event.inputs.tag == 'latest' && matrix.BASE == '20-jammy-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

6
.gitmodules vendored
View File

@@ -4,12 +4,6 @@
[submodule "plugins/myq/src/myq"]
path = plugins/myq/src/myq
url = ../../koush/myq.git
[submodule "plugins/tensorflow/face-api.js"]
path = external/face-api.js
url = ../../koush/face-api.js
[submodule "external/scrypted-ffmpeg"]
path = external/scrypted-ffmpeg
url = ../../koush/scrypted-ffmpeg
[submodule "external/ring-client-api"]
path = external/ring-client-api
url = ../../koush/ring

1
common/fs/@types/sdk/settings-mixin.d.ts vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../sdk/dist/src/settings-mixin.d.ts

View File

@@ -0,0 +1 @@
../../../../sdk/dist/src/storage-settings.d.ts

View File

@@ -73,5 +73,5 @@ export abstract class AutoenableMixinProvider extends ScryptedDeviceBase {
this.storage.setItem('hasEnabledMixin', JSON.stringify(this.hasEnabledMixin));
}
abstract canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]>;
abstract canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[] | null | undefined | void>;
}

View File

@@ -1,9 +1,10 @@
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors } from "@scrypted/sdk";
import sdk, { MixinDeviceBase, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedInterfaceDescriptors, ScryptedMimeTypes } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import { SettingsMixinDeviceBase } from "@scrypted/sdk/settings-mixin";
import fs from 'fs';
import type { TranspileOptions } from "typescript";
import vm from "vm";
import { ScriptDevice } from "./monaco/script-device";
import path from 'path';
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
@@ -28,9 +29,13 @@ export function readFileAsString(f: string) {
}
function getTypeDefs() {
const settingsMixinDefs = readFileAsString('@types/sdk/settings-mixin.d.ts');
const storageSettingsDefs = readFileAsString('@types/sdk/storage-settings.d.ts');
const scryptedTypesDefs = readFileAsString('@types/sdk/types.d.ts');
const scryptedIndexDefs = readFileAsString('@types/sdk/index.d.ts');
return {
settingsMixinDefs,
storageSettingsDefs,
scryptedIndexDefs,
scryptedTypesDefs,
};
@@ -64,6 +69,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
fs: require('realfs'),
ScryptedDeviceBase,
MixinDeviceBase,
StorageSettings,
systemManager,
deviceManager,
endpointManager,
@@ -73,6 +79,8 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
localStorage: device.storage,
device,
exports: {} as any,
SettingsMixinDeviceBase,
ScryptedMimeTypes,
ScryptedInterface,
ScryptedDeviceType,
// @ts-expect-error
@@ -109,11 +117,20 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
}
export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
const bufferTypeDefs= readFileAsString('@types/node/buffer.d.ts');
const safeLibs: any = {};
const safeLibs = {
bufferTypeDefs,
};
for (const safeLib of [
'@types/node/globals.d.ts',
'@types/node/buffer.d.ts',
'@types/node/process.d.ts',
'@types/node/events.d.ts',
'@types/node/stream.d.ts',
'@types/node/fs.d.ts',
'@types/node/net.d.ts',
'@types/node/child_process.d.ts',
]) {
safeLibs[`node_modules/${safeLib}`] = readFileAsString(safeLib)
}
const libs = Object.assign(getTypeDefs(), extraLibs);
@@ -164,15 +181,27 @@ export function createMonacoEvalDefaults(extraLibs: { [lib: string]: string }) {
"node_modules/@types/scrypted__sdk/types/index.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['settingsMixin'],
"node_modules/@types/scrypted__sdk/settings-mixin.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['storageSettings'],
"node_modules/@types/scrypted__sdk/storage-settings.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
libs['sdk'],
"node_modules/@types/scrypted__sdk/index.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
safeLibs.bufferTypeDefs,
"node_modules/@types/node/buffer.d.ts"
);
for (const lib of Object.keys(safeLibs)) {
monaco.languages.typescript.typescriptDefaults.addExtraLib(
safeLibs[lib],
lib,
);
}
}
return `(function() {

View File

@@ -136,12 +136,17 @@ export async function readLine(readable: Readable) {
}
export async function readString(readable: Readable | Promise<Readable>) {
let data = '';
const buffer = await readBuffer(readable);
return buffer.toString();
}
export async function readBuffer(readable: Readable | Promise<Readable>) {
const buffers: Buffer[] = [];
readable = await readable;
readable.on('data', buffer => {
data += buffer.toString();
buffers.push(buffer);
});
readable.resume();
await once(readable, 'end')
return data;
return Buffer.concat(buffers);
}

View File

@@ -223,7 +223,12 @@ export class BrowserSignalingSession implements RTCSignalingSession {
}
if (type === 'offer') {
let offer = await this.pc.createOffer({
let offer: RTCSessionDescriptionInit = this.pc.localDescription;
if (offer) {
// fast path for duplicate calls to createLocalDescription
return toDescription(this.pc.localDescription);
}
offer = await this.pc.createOffer({
offerToReceiveAudio: !!setup.audio,
offerToReceiveVideo: !!setup.video,
});
@@ -232,7 +237,7 @@ export class BrowserSignalingSession implements RTCSignalingSession {
return toDescription(offer);
await set;
await gatheringPromise;
offer = await this.pc.createOffer({
offer = this.pc.localDescription || await this.pc.createOffer({
offerToReceiveAudio: !!setup.audio,
offerToReceiveVideo: !!setup.video,
});

View File

@@ -324,6 +324,7 @@ export class RtspClient extends RtspBase {
setupOptions = new Map<number, RtspClientTcpSetupOptions>();
issuedTeardown = false;
hasGetParameter = true;
contentBase: string;
constructor(public url: string) {
super();
@@ -364,13 +365,18 @@ export class RtspClient extends RtspBase {
async writeRequest(method: string, headers?: Headers, path?: string, body?: Buffer) {
headers = headers || {};
let fullUrl = this.url;
if (path) {
let fullUrl: string;
if (!path) {
fullUrl = this.url;
}
else {
// a=control may be a full or "relative" url.
if (path.includes('rtsp://') || path.includes('rtsps://')) {
if (path.includes('rtsp://') || path.includes('rtsps://') || path === '*') {
fullUrl = path;
}
else {
fullUrl = this.contentBase || this.url;
// strangely, relative RTSP urls do not behave like expected from an HTTP-ish server.
// ffmpeg will happily suffix path segments after query strings:
// SETUP rtsp://localhost:5554/cam/realmonitor?channel=1&subtype=0/trackID=0 RTSP/1.0
@@ -645,10 +651,13 @@ export class RtspClient extends RtspBase {
}
async describe(headers?: Headers) {
return this.request('DESCRIBE', {
const response = await this.request('DESCRIBE', {
...(headers || {}),
Accept: 'application/sdp',
});
this.contentBase = response.headers['content-base'] || response.headers['content-location'];;
return response;
}
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions, headers?: Headers) {

View File

@@ -172,7 +172,8 @@ export function parseFmtp(msection: string[]) {
export type MSection = ReturnType<typeof parseMSection>;
export type RTPMap = ReturnType<typeof parseRtpMap>;
export function parseRtpMap(mlineType: string, rtpmap: string) {
export function parseRtpMap(mline: ReturnType<typeof parseMLine>, rtpmap: string) {
const mlineType = mline.type;
const match = rtpmap?.match(/a=rtpmap:([\d]+) (.*?)\/([\d]+)(\/([\d]+))?/);
rtpmap = rtpmap?.toLowerCase();
@@ -218,9 +219,23 @@ export function parseRtpMap(mlineType: string, rtpmap: string) {
codec = 'h265';
}
else if (!rtpmap && mlineType === 'audio') {
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
// is this the default?
codec = 'pcm_alaw';
if (mline.payloadTypes?.includes(0)) {
codec = 'pcm_mulaw';
ffmpegEncoder = 'pcm_mulaw';
}
else if (mline.payloadTypes?.includes(8)) {
codec = 'pcm_alaw';
ffmpegEncoder = 'pcm_alaw';
}
else {
// ffmpeg seems to omit the rtpmap type for pcm alaw when creating sdp?
// is this the default?
// 2/21/2024: the paylaod types are included in the mline, and this is legacy code
// that maybe should be updated to use the mline payload types when no rtpmap(s) are available.
// https://en.wikipedia.org/wiki/RTP_payload_formats
codec = 'pcm_alaw';
ffmpegEncoder = 'pcm_alaw';
}
}
return {
@@ -240,9 +255,9 @@ export function parseMSection(msection: string[]) {
const control = msection.find(line => line.startsWith(acontrol))?.substring(acontrol.length);
const mline = parseMLine(msection[0]);
const rawRtpmaps = msection.filter(line => line.startsWith(artpmap));
const rtpmaps = rawRtpmaps.map(line => parseRtpMap(mline.type, line));
const rtpmaps = rawRtpmaps.map(line => parseRtpMap(mline, line));
// if no rtp map is specified, pcm_alaw is used. parsing a null rtpmap is valid.
const rtpmap = parseRtpMap(mline.type, rawRtpmaps[0]);
const rtpmap = parseRtpMap(mline, rawRtpmaps[0]);
const { codec } = rtpmap;
let direction: string;

View File

@@ -1,6 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "Node16",
"moduleResolution": "Node16",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",

Submodule external/face-api.js deleted from a86687eea2

View File

@@ -1,6 +1,6 @@
# Home Assistant Addon Configuration
name: Scrypted
version: "18-jammy-full.s6-v0.91.6"
version: "20-jammy-full.s6-v0.97.0"
slug: scrypted
description: Scrypted is a high performance home video integration and automation platform
url: "https://github.com/koush/scrypted"

View File

@@ -1,4 +1,4 @@
ARG BASE="18-jammy-full"
ARG BASE="20-jammy-full"
FROM ghcr.io/koush/scrypted-common:${BASE}
WORKDIR /
@@ -16,6 +16,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20241303"
ENV SCRYPTED_BASE_VERSION="20240321"
CMD npm --prefix /server exec scrypted-serve

View File

@@ -7,7 +7,8 @@
# install script.
################################################################
ARG BASE="jammy"
FROM ubuntu:${BASE} as header
ARG REPO="ubuntu"
FROM ${REPO}:${BASE} as header
ENV DEBIAN_FRONTEND=noninteractive
@@ -24,7 +25,7 @@ RUN apt-get update && apt-get -y install \
apt-get -y update && \
apt-get -y upgrade
ARG NODE_VERSION=18
ARG NODE_VERSION=20
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
@@ -99,8 +100,11 @@ ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
ENV SCRYPTED_DOCKER_FLAVOR="full"

View File

@@ -6,36 +6,27 @@ ENV DEBIAN_FRONTEND=noninteractive
# base tools and development stuff
RUN apt-get update && apt-get -y install \
curl software-properties-common apt-utils \
ffmpeg && \
python3 && \
apt-get -y update && \
apt-get -y upgrade
ARG NODE_VERSION=18
ARG NODE_VERSION=20
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_"$NODE_VERSION".x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
# python native
RUN apt-get -y install \
python3 \
python3-dev \
python3-pip \
python3-setuptools \
python3-wheel
# python pip
RUN rm -f /usr/lib/python**/EXTERNALLY-MANAGED
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install debugpy typing_extensions psutil
# intel opencl gpu for openvino
RUN curl https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-intel-graphics.sh | bash
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
RUN test -f "/usr/bin/python3" && test -f "/usr/bin/python3.10"
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
ENV SCRYPTED_DOCKER_FLAVOR="lite"

View File

@@ -1,14 +1,14 @@
FROM ghcr.io/koush/scrypted:18-jammy-full.s6
FROM ghcr.io/koush/scrypted:20-jammy-full.s6
WORKDIR /
# Install miniconda
ENV CONDA_DIR /opt/conda
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
RUN apt update -y && apt -y install wget && wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
/bin/bash ~/miniconda.sh -b -p /opt/conda
# Put conda in path so we can use conda activate
ENV PATH=$CONDA_DIR/bin:$PATH
RUN conda install -c conda-forge cudatoolkit=11.2.2 cudnn=8.1.0
RUN conda -y install -c conda-forge cudatoolkit cudnn
ENV CONDA_PREFIX=/opt/conda
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/

View File

@@ -1,4 +1,4 @@
ARG BASE="18-jammy-full"
ARG BASE="20-jammy-full"
FROM ghcr.io/koush/scrypted-common:${BASE}
# avahi advertiser support
@@ -46,6 +46,6 @@ ENV NODE_OPTIONS="--dns-result-order=ipv4first"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION="20241303"
ENV SCRYPTED_BASE_VERSION="20240321"
CMD npm --prefix /server exec scrypted-serve

View File

@@ -1,22 +0,0 @@
ARG BASE="jammy"
FROM ubuntu:${BASE} as header
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y update && \
apt-get -y upgrade && \
apt-get -y install curl software-properties-common apt-utils ffmpeg
# switch to nvm?
ARG NODE_VERSION=18
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_VERSION.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
ENV SCRYPTED_INSTALL_ENVIRONMENT="docker"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
ENV SCRYPTED_DOCKER_FLAVOR="thin"

View File

@@ -1,3 +1,3 @@
./docker-build.sh
docker build -t ghcr.io/koush/scrypted:18-jammy-full.nvidia -f Dockerfile.nvidia .
docker build -t ghcr.io/koush/scrypted:20-jammy-full.nvidia -f Dockerfile.nvidia .

View File

@@ -2,7 +2,7 @@
set -x
NODE_VERSION=18
NODE_VERSION=20
SCRYPTED_INSTALL_VERSION=beta
IMAGE_BASE=jammy
FLAVOR=full

View File

@@ -34,16 +34,19 @@ services:
- SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer SET_THIS_TO_SOME_RANDOM_TEXT
- SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update
# Uncomment next line to run avahi-daemon inside the container
# Don't use if dbus and avahi run on the host and are bind-mounted
# (see below under "volumes")
# Avahi can be used for network discovery by passing in the host daemon
# or running the daemon inside the container. Choose one or the other.
# Uncomment next line to run avahi-daemon inside the container.
# See volumes section below to use the host daemon.
# - SCRYPTED_DOCKER_AVAHI=true
# Uncomment next 3 lines for Nvidia GPU support.
# - NVIDIA_VISIBLE_DEVICES=all
# - NVIDIA_DRIVER_CAPABILITIES=all
# runtime: nvidia
# Necessary to communicate with host dbus for avahi-daemon.
security_opt:
- apparmor:unconfined
volumes:
# Scrypted NVR Storage (Part 3 of 3)
@@ -59,9 +62,10 @@ services:
# volume:
# nocopy: true
# uncomment the following lines to expose Avahi, an mDNS advertiser.
# make sure Avahi is running on the host machine, otherwise this will not work.
# not compatible with Avahi enabled via SCRYPTED_DOCKER_AVAHI=true
# Uncomment the following lines to use Avahi daemon from the host.
# Ensure Avahi is running on the host machine:
# It can be installed with: sudo apt-get install avahi-daemon
# This is not compatible with running avahi inside the container (see above).
# - /var/run/dbus:/var/run/dbus
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket

View File

@@ -55,6 +55,14 @@ then
sed -i 's/'#' "\/dev\/dri/"\/dev\/dri/g' $DOCKER_COMPOSE_YML
fi
readyn "Install avahi-daemon? This is the recommended for reliable HomeKit discovery and pairing."
if [ "$yn" == "y" ]
then
sudo apt-get -y install avahi-daemon
sed -i 's/'#' - \/var\/run\/dbus/- \/var\/run\/dbus/g' $DOCKER_COMPOSE_YML
sed -i 's/'#' - \/var\/run\/avahi-daemon/- \/var\/run\/avahi-daemon/g' $DOCKER_COMPOSE_YML
fi
echo "Setting permissions on $SCRYPTED_HOME"
chown -R $SERVICE_USER $SCRYPTED_HOME

View File

@@ -59,12 +59,19 @@ then
fi
function stopscrypted() {
cd "$SCRYPTED_HOME"
cd $SCRYPTED_HOME
echo ""
echo "Stopping the Scrypted container. If there are any errors during disk setup, Scrypted will need to be manually restarted with:"
echo "cd $SCRYPTED_HOME && docker compose up -d"
echo ""
docker compose down
sudo -u $SERVICE_USER docker compose down 2> /dev/null
}
function removescryptedfstab() {
backup "/etc/fstab"
grep -v "scrypted-nvr" /etc/fstab > /tmp/fstab && cp /tmp/fstab /etc/fstab
# ensure newline
sed -i -e '$a\' /etc/fstab
}
BLOCK_DEVICE="/dev/$1"
@@ -108,12 +115,9 @@ then
echo "UUID: $UUID"
set -e
backup "/etc/fstab"
grep -v "scrypted-nvr" /etc/fstab > /tmp/fstab && cp /tmp/fstab /etc/fstab
# ensure newline
sed -i -e '$a\' /etc/fstab
removescryptedfstab
mkdir -p /mnt/scrypted-nvr
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults 0 0" >> /etc/fstab
echo "PARTLABEL=scrypted-nvr /mnt/scrypted-nvr ext4 defaults,nofail 0 0" >> /etc/fstab
mount -a
set +e
@@ -127,6 +131,8 @@ else
stopscrypted
removescryptedfstab
DIR="$1"
fi
@@ -137,4 +143,5 @@ sed -i s/'^.*:\/nvr'/" - $ESCAPED_DIR:\/nvr"/ "$DOCKER_COMPOSE_YML"
sed -i s/'^.*SCRYPTED_NVR_VOLUME.*$'/" - SCRYPTED_NVR_VOLUME=\/nvr"/ "$DOCKER_COMPOSE_YML"
set +e
cd "$SCRYPTED_HOME" && docker compose up -d
cd $SCRYPTED_HOME
sudo -u $SERVICE_USER docker compose up -d

View File

@@ -31,8 +31,11 @@ ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
RUN test -f "/usr/bin/ffmpeg"
RUN test -f "/usr/bin/ffmpeg" && test -f "/usr/bin/python3" && test -f "/usr/bin/python3.9" && test -f "/usr/bin/python3.10"
ENV SCRYPTED_FFMPEG_PATH="/usr/bin/ffmpeg"
ENV SCRYPTED_PYTHON_PATH="/usr/bin/python3"
ENV SCRYPTED_PYTHON39_PATH="/usr/bin/python3.9"
ENV SCRYPTED_PYTHON310_PATH="/usr/bin/python3.10"
ENV SCRYPTED_DOCKER_FLAVOR="full"

View File

@@ -21,7 +21,7 @@ RUN apt-get update && apt-get -y install \
apt-get -y update && \
apt-get -y upgrade
ARG NODE_VERSION=18
ARG NODE_VERSION=20
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg

View File

@@ -39,7 +39,7 @@ launchctl unload ~/Library/LaunchAgents/app.scrypted.server.plist || echo ""
echo "Installing Scrypted dependencies..."
RUN_IGNORE xcode-select --install
RUN brew update
RUN_IGNORE brew install node@18
RUN_IGNORE brew install node@20
# snapshot plugin and others
RUN brew install libvips
# dlib
@@ -81,17 +81,17 @@ echo "Installing Scrypted Launch Agent..."
RUN mkdir -p ~/Library/LaunchAgents
NODE_PATH=$(brew --prefix node@18)
NODE_PATH=$(brew --prefix node@20)
if [ ! -d "$NODE_PATH" ]
then
echo "Unable to determine node@18 path."
echo "Unable to determine node@20 path."
exit 1
fi
NODE_BIN_PATH=$NODE_PATH/bin
if [ ! -d "$NODE_BIN_PATH" ]
then
echo "Unable to determine node@18 bin path."
echo "Unable to determine node@20 bin path."
echo "$NODE_BIN_PATH does not exist."
exit 1
fi

View File

@@ -8,8 +8,12 @@ sc.exe stop scrypted.exe
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
# Install node.js
choco upgrade -y nodejs-lts --version=18.14.0
choco upgrade -y nodejs-lts --version=20.11.1
# Install VC Redist, which is necessary for portable python
choco install vcredist140
# TODO: remove python install, and use portable python
# Install Python
choco upgrade -y python39
# Run py.exe with a specific version

View File

@@ -10,7 +10,7 @@ function readyn() {
}
cd /tmp
SCRYPTED_VERSION=v0.80.0
SCRYPTED_VERSION=v0.96.0
SCRYPTED_TAR_ZST=scrypted-$SCRYPTED_VERSION.tar.zst
if [ -z "$VMID" ]
then

View File

@@ -1,3 +1,4 @@
#!/bin/bash
echo 'if (!process.version.startsWith("v18")) throw new Error("Node 18 is required. Install Node Version Manager (nvm) for versioned node installations. See https://github.com/koush/scrypted/pull/498#issuecomment-1373854020")' | node
if [ "$?" != 0 ]
then

View File

@@ -1,12 +1,12 @@
{
"name": "scrypted",
"version": "1.3.10",
"version": "1.3.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.3.10",
"version": "1.3.14",
"license": "ISC",
"dependencies": {
"@scrypted/client": "^1.3.3",

View File

@@ -1,6 +1,6 @@
{
"name": "scrypted",
"version": "1.3.10",
"version": "1.3.14",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"bin": {

View File

@@ -117,6 +117,10 @@ export async function serveMain(installVersion?: string) {
await installServe(installVersion, true);
}
// todo: remove at some point after core lxc updater rolls out.
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT === 'lxc')
process.env.SCRYPTED_FFMPEG_PATH = '/usr/bin/ffmpeg';
process.env.SCRYPTED_NPM_SERVE = 'true';
process.env.SCRYPTED_VOLUME = volume;
process.env.SCRYPTED_CAN_EXIT = 'true';
@@ -129,16 +133,20 @@ export async function serveMain(installVersion?: string) {
await startServer(installDir);
if (fs.existsSync(EXIT_FILE)) {
console.log('Exiting.');
process.exit();
}
else if (fs.existsSync(UPDATE_FILE)) {
if (fs.existsSync(UPDATE_FILE)) {
console.log('Update requested. Installing.');
await runCommandEatError('npm', '--prefix', installDir, 'install', '--production', '@scrypted/server@latest');
await runCommandEatError('npm', '--prefix', installDir, 'install', '--production', '@scrypted/server@latest').catch(e => {
console.error('Update failed', e);
});
console.log('Exiting.');
process.exit(1);
}
else if (fs.existsSync(EXIT_FILE)) {
console.log('Exiting.');
process.exit(1);
}
else {
console.log(`Service exited. Restarting momentarily.`);
console.log(`Service unexpectedly exited. Restarting momentarily.`);
await sleep(10000);
}
}

View File

@@ -1,24 +1,24 @@
{
"name": "@scrypted/client",
"version": "1.3.4",
"version": "1.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.3.4",
"version": "1.3.5",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.3.4",
"@scrypted/types": "^0.3.27",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.4",
"follow-redirects": "^1.15.6",
"rimraf": "^5.0.5"
},
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^20.10.8",
"@types/node": "^20.11.30",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.4.3"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -84,9 +84,9 @@
}
},
"node_modules/@scrypted/types": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.4.tgz",
"integrity": "sha512-k/YMx8lIWOkePgXfKW9POr12mb+erFU2JKxO7TW92GyW8ojUWw9VOc0PK6O9bybi0vhsEnvMFkO6pO6bAonsVA=="
"version": "0.3.27",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.3.27.tgz",
"integrity": "sha512-XNtlqzqt6rHyNYwWrz3iiickh1h9ACwcLC3rfwxUbFk/Vq/UbDZgp0kGyj9UW6eLVNHzWFSE2dKqyyDS6V2KAg=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -127,9 +127,9 @@
}
},
"node_modules/@types/node": {
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -288,9 +288,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -615,9 +615,9 @@
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
"dev": true,
"bin": {
"tsc": "bin/tsc",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.3.4",
"version": "1.3.5",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -13,14 +13,14 @@
"license": "ISC",
"devDependencies": {
"@types/ip": "^1.1.3",
"@types/node": "^20.10.8",
"@types/node": "^20.11.30",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.4.3"
},
"dependencies": {
"@scrypted/types": "^0.3.4",
"@scrypted/types": "^0.3.27",
"engine.io-client": "^6.5.3",
"follow-redirects": "^1.15.4",
"follow-redirects": "^1.15.6",
"rimraf": "^5.0.5"
}
}

View File

@@ -1,4 +1,4 @@
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import { MediaObjectCreateOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
import { Deferred } from "../../../common/src/deferred";
@@ -316,15 +316,9 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
}
}
// the alternate urls must have a valid response.
const loginCheckPromises = [...urlsToCheck].map(async baseUrl => {
const loginCheck = await checkScryptedClientLogin({
baseUrl,
previousLoginResult: options?.previousLoginResult,
});
function validateLoginResult(loginCheck: Awaited<ReturnType<typeof checkScryptedClientLogin>>) {
if (loginCheck.error || loginCheck.redirect)
throw new Error('login error');
throw new ScryptedClientLoginError(loginCheck);
if (!loginCheck.authorization || !loginCheck.username || !loginCheck.queryToken) {
console.error(loginCheck);
@@ -332,11 +326,22 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
}
return loginCheck;
}
// the alternate urls must have a valid response.
const loginCheckPromises = [...urlsToCheck].map(baseUrl => {
return checkScryptedClientLogin({
baseUrl,
previousLoginResult: options?.previousLoginResult,
})
.then(validateLoginResult);
});
const baseUrlCheck = checkScryptedClientLogin({
baseUrl,
});
previousLoginResult: options?.previousLoginResult,
})
.then(validateLoginResult);
loginCheckPromises.push(baseUrlCheck);
let loginCheck: Awaited<ReturnType<typeof checkScryptedClientLogin>>;
@@ -686,7 +691,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
} = scrypted;
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
mediaManager.createMediaObject = async<T extends MediaObjectCreateOptions>(data: any, mimeType: string, options: T) => {
return new MediaObject(mimeType, data, options) as any;
}
@@ -865,6 +870,8 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
cloudAddress,
},
connectRPCObject,
fork: undefined,
connect: undefined,
}
socket.on('close', () => {

View File

@@ -1,4 +1,4 @@
{
"scrypted.debugHost": "koushik-ubuntu",
"scrypted.debugHost": "127.0.0.1",
}

153
plugins/alexa/CHANGELOG.md Normal file
View File

@@ -0,0 +1,153 @@
<details>
<summary>Changelog</summary>
### 0.3.1
alexa/google-home: fix potential vulnerability. do not allow local network control using cloud tokens belonging to a different user. the plugins are now locked to a specific scrypted cloud account once paired.
### 0.3.0
alexa/google-home: additional auth token checks to harden endpoints for cloud sharing
alexa: removed unneeded packages (#1319)
alexa: added support for `light`, `outlet`, and `fan` device types (#1318)
### 0.2.10
alexa: fix potential response race
### 0.2.9
alexa: fix race condition in sendResponse
### 0.2.8
alexa: display camera on doorbell press (#1066)
### 0.2.7
alexa: added helpful error messages regarding token expiration (#1007)
### 0.2.6
alexa: fix doorbells
### 0.2.5
alexa: publish w/ storage fix
### 0.2.4
alexa: add setting to publish debug events to console (#685)
### 0.2.3
webrtc/alexa: add option to disable TURN on peers that already have externally reachable addresses
### 0.2.1
alexa: set screen ratio to 720p (#625)
### 0.2.0
alexa: refactor code structure (#606)
### 0.1.0
alexa: ensure we are talking to the correct API endpoint (#580)
### 0.0.20
alexa: provide hint that medium resolution is always used.
### 0.0.19
various: minor cleanups
alexa: added logging around `tokenInfo` resets (#488)
sdk: rename sdk.version to sdk.serverVersion
plugins: update tsconfig.json
alexa: publish beta
### 0.0.18
alexa: rethrow login failure error
added support for type `Garage` and refactored the controller for future support (#479)
updated install instructions (#478)
webrtc/alexa: fix race condition with intercoms and track not received yet.
### 0.0.17
alexa: close potential security hole if scrypted is exposed to the internet directly (ie, user is not using the cloud plugin against recommendations)
### 0.0.16
plugins: remove postinstall
plugins: add tsconfig.json
alexa: doorbell motion sensor support
### 0.0.15
alexa: fix harmless crash in log
### 0.0.14
alexa: fix empty endpoint list
### 0.0.13
all: prune package.json
alexa: fix doorbell syncing
### 0.0.12
alexa: publish
### 0.0.10
alexa: 2 way audio
### 0.0.4
alexa: 2 way audio
alexa: motion events
### 0.0.3
webrtc: refactor
alexa: use rtc signaling channel
alexa: publish
### 0.0.1
alexa: doorbells
alexa: sync devices properly
alexa: add camera/doorbell, fix webrtc to work with amazon reqs
alexa: initial pass with working cameras
cloud: stub out alexa
</details>

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/alexa",
"version": "0.2.10",
"version": "0.3.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.2.10",
"version": "0.3.2",
"dependencies": {
"axios": "^1.3.4",
"uuid": "^9.0.0"
@@ -14,33 +14,35 @@
"devDependencies": {
"@scrypted/common": "../../common",
"@scrypted/sdk": "../../sdk",
"@types/debug": "^4.1.12",
"@types/node": "^18.4.2"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"dev": true,
"license": "ISC",
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.108",
"version": "0.3.5",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -81,6 +83,21 @@
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
}
},
"node_modules/@types/ms": {
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
@@ -122,9 +139,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"funding": [
{
"type": "individual",

View File

@@ -1,18 +1,19 @@
{
"name": "@scrypted/alexa",
"version": "0.2.10",
"version": "0.3.2",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
"build": "scrypted-webpack",
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
"prepublishOnly": "scrypted-changelog && NODE_ENV=production scrypted-webpack",
"prescrypted-vscode-launch": "scrypted-webpack",
"scrypted-vscode-launch": "scrypted-deploy-debug",
"scrypted-deploy-debug": "scrypted-deploy-debug",
"scrypted-debug": "scrypted-debug",
"scrypted-deploy": "scrypted-deploy",
"scrypted-readme": "scrypted-readme",
"scrypted-package-json": "scrypted-package-json"
"scrypted-changelog": "scrypted-changelog",
"scrypted-package-json": "scrypted-package-json",
"scrypted-readme": "scrypted-readme"
},
"keywords": [
"scrypted",
@@ -38,8 +39,9 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/node": "^18.4.2",
"@scrypted/common": "../../common",
"@scrypted/sdk": "../../sdk",
"@scrypted/common": "../../common"
"@types/debug": "^4.1.12",
"@types/node": "^18.4.2"
}
}

View File

@@ -1,4 +1,4 @@
export declare type DisplayCategory = 'ACTIVITY_TRIGGER' | 'CAMERA' | 'CONTACT_SENSOR' | 'DOOR' | 'DOORBELL' | 'GARAGE_DOOR' | 'LIGHT' | 'MICROWAVE' | 'MOTION_SENSOR' | 'OTHER' | 'SCENE_TRIGGER' | 'SECURITY_PANEL' | 'SMARTLOCK' | 'SMARTPLUG' | 'SPEAKER' | 'SWITCH' | 'TEMPERATURE_SENSOR' | 'THERMOSTAT' | 'TV';
export declare type DisplayCategory = 'ACTIVITY_TRIGGER' | 'CAMERA' | 'CONTACT_SENSOR' | 'DOOR' | 'DOORBELL' | 'GARAGE_DOOR' | 'LIGHT' | 'MICROWAVE' | 'MOTION_SENSOR' | 'OTHER' | 'SCENE_TRIGGER' | 'SECURITY_PANEL' | 'SMARTLOCK' | 'SMARTPLUG' | 'SPEAKER' | 'SWITCH' | 'TEMPERATURE_SENSOR' | 'THERMOSTAT' | 'TV' | 'FAN';
/*
COMMON DIRECTIVES AND RESPONSES
@@ -116,7 +116,7 @@ export interface ErrorPayload {
message: string;
}
export interface ChangePayload {
export interface ChangePayload extends Payload {
change: {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION" | "RULE_TRIGGER";

View File

@@ -27,6 +27,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
json: true
},
syncedDevices: {
defaultValue: [],
multiple: true,
hide: true
},
@@ -47,7 +48,11 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
onPut(oldValue: boolean, newValue: boolean) {
DEBUG = newValue;
}
}
},
pairedUserId: {
title: "Pairing Key",
description: "The pairing key used to validate requests from Alexa. Clear this key or delete the plugin to allow pairing with a different Alexa login.",
},
});
accessToken: Promise<string>;
@@ -62,7 +67,10 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
this.start();
this.start()
.catch(e => {
this.console.error('startup failed', e);
})
}
async start() {
@@ -167,7 +175,7 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
}
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
debug(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
@@ -599,11 +607,22 @@ class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, Mixi
try {
debug("making authorization request to Scrypted");
await axios.get('https://home.scrypted.app/_punch/getcookie', {
const getcookieResponse = await axios.get('https://home.scrypted.app/_punch/getcookie', {
headers: {
'Authorization': authorization,
}
});
// new tokens will contain a lot of information, including the expiry and client id.
// validate this. old tokens will be grandfathered in.
if (getcookieResponse.data.expiry && getcookieResponse.data.clientId !== 'amazon')
throw new Error('client id mismatch');
if (!this.storageSettings.values.pairedUserId) {
this.storageSettings.values.pairedUserId = getcookieResponse.data.id;
}
else if (this.storageSettings.values.pairedUserId !== getcookieResponse.data.id) {
this.log.a('This plugin is already paired with a different account. Clear the existing key in the plugin settings to pair this plugin with a different account.');
throw new Error('user id mismatch');
}
this.validAuths.add(authorization);
}
catch (e) {

View File

@@ -0,0 +1,71 @@
import { OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, ChangeReport, Report, Property, ChangePayload, DiscoveryCapability } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Fan, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.OnOff))
return;
const capabilities: DiscoveryCapability[] = [];
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
});
return {
displayCategories: ['FAN'],
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & OnOff): Promise<Partial<Report>> {
return {
context: {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventSource.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
};
},
async sendEvent(eventSource: ScryptedDevice & OnOff, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.OnOff)
return undefined;
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventData ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
}
});

View File

@@ -13,8 +13,12 @@ export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
import '../handlers';
import './camera';
import './camera/handlers';
import './light';
import './light/handlers'
import './fan';
import './doorbell';
import './garagedoor';
import './outlet';
import './switch';
import './switch/handlers';
import './sensor';

View File

@@ -0,0 +1,225 @@
import { Brightness, ColorSettingHsv, ColorSettingTemperature, OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, ChangeReport, Report, Property, ChangePayload, DiscoveryCapability, StateReport } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Light, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.OnOff))
return;
const capabilities: DiscoveryCapability[] = [];
if (device.interfaces.includes(ScryptedInterface.OnOff)) {
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [{
"name": "powerState"
}],
"proactivelyReported": true,
"retrievable": true
}
});
}
if (device.interfaces.includes(ScryptedInterface.Brightness)) {
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.BrightnessController",
"version": "3",
"properties": {
"supported": [{
"name": "brightness"
}],
"proactivelyReported": true,
"retrievable": true
}
});
}
if (device.interfaces.includes(ScryptedInterface.ColorSettingTemperature)) {
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.ColorTemperatureController",
"version": "3",
"properties": {
"supported": [{
"name": "colorTemperatureInKelvin"
}],
"proactivelyReported": true,
"retrievable": true
}
});
}
if (device.interfaces.includes(ScryptedInterface.ColorSettingHsv)) {
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.ColorController",
"version": "3",
"properties": {
"supported": [{
"name": "color"
}],
"proactivelyReported": true,
"retrievable": true
}
});
}
return {
displayCategories: ['LIGHT'],
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & OnOff & Brightness & ColorSettingHsv & ColorSettingTemperature): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.OnOff)) {
data.context.properties.push({
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventSource.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.Brightness)) {
data.context.properties.push({
"namespace": "Alexa.BrightnessController",
"name": "brightness",
"value": eventSource.brightness,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.ColorSettingHsv) && eventSource.hsv) {
data.context.properties.push({
"namespace": "Alexa.ColorController",
"name": "color",
"value": {
"hue": eventSource.hsv.h,
"saturation": eventSource.hsv.s,
"brightness": eventSource.hsv.v
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.ColorSettingTemperature) && eventSource.colorTemperature) {
data.context.properties.push({
"namespace": "Alexa.ColorTemperatureController",
"name": "colorTemperatureInKelvin",
"value": eventSource.colorTemperature,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & OnOff & Brightness & ColorSettingHsv & ColorSettingTemperature, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface == ScryptedInterface.OnOff)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventData ? "ON" : "OFF",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface == ScryptedInterface.Brightness && eventSource.brightness)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.BrightnessController",
"name": "brightness",
"value": eventSource.brightness,
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface == ScryptedInterface.ColorSettingHsv && eventSource.hsv)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.ColorController",
"name": "color",
"value": {
"hue": eventSource.hsv.h,
"saturation": eventSource.hsv.s,
"brightness": eventSource.hsv.v
},
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface == ScryptedInterface.ColorSettingTemperature && eventSource.colorTemperature)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.ColorTemperatureController",
"name": "colorTemperatureInKelvin",
"value": eventSource.colorTemperature,
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
return undefined;
}
});

View File

@@ -0,0 +1,166 @@
import { Brightness, ColorHsv, ColorSettingHsv, ColorSettingTemperature, ScryptedDevice, ScryptedInterface } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { deviceErrorResponse, sendDeviceResponse } from "../../common";
import { v4 as createMessageId } from 'uuid';
import { alexaDeviceHandlers } from "../../handlers";
import { Directive, Response } from "../../alexa";
import { error } from "console";
function commonBrightnessResponse(header, endpoint, payload, response, device: ScryptedDevice & Brightness) {
const data : Response = {
"event": {
header,
endpoint,
payload
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "brightness",
"value": device.brightness,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
};
data.event.header.namespace = "Alexa";
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.BrightnessController/SetBrightness', async (request, response, directive: Directive, device: ScryptedDevice & Brightness) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.setBrightness((payload as any).brightness)
commonBrightnessResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.BrightnessController/AdjustBrightness', async (request, response, directive: Directive, device: ScryptedDevice & Brightness) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.setBrightness(device.brightness + (payload as any).brightnessDelta)
commonBrightnessResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.ColorController/SetColor', async (request, response, directive: Directive, device: ScryptedDevice & ColorSettingHsv) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
let hsv : ColorHsv = {
h: (payload as any).color.hue,
s: (payload as any).color.saturation,
v: (payload as any).color.brightness
};
if (!device.interfaces.includes(ScryptedInterface.ColorSettingHsv))
return deviceErrorResponse("INVALID_REQUEST_EXCEPTION", "Device does not support setting color by HSV.", directive);
await device.setHsv(hsv.h, hsv.s, hsv.v);
hsv = device.hsv;
const data : Response = {
"event": {
"header": {
"namespace": "Alexa",
"name": "Response",
"messageId": createMessageId(),
"correlationToken": header.correlationToken,
"payloadVersion": header.payloadVersion
},
endpoint,
payload
},
"context": {
"properties": [
{
"namespace": "Alexa.ColorController",
"name": "color",
"value": {
"hue": hsv.h,
"saturation": hsv.s,
"brightness": hsv.v
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
};
sendDeviceResponse(data, response, device);
});
function commonColorTemperatureResponse(header, endpoint, payload, response, device: ScryptedDevice & ColorSettingTemperature) {
const data : Response = {
"event": {
header,
endpoint,
payload
},
"context": {
"properties": [
{
"namespace": "Alexa.ColorTemperatureController",
"name": "colorTemperatureInKelvin",
"value": device.colorTemperature,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
};
data.event.header.namespace = "Alexa";
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.ColorTemperatureController/SetColorTemperature', async (request, response, directive: Directive, device: ScryptedDevice & ColorSettingTemperature) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.setColorTemperature((payload as any).colorTemperatureInKelvin)
commonColorTemperatureResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.ColorTemperatureController/IncreaseColorTemperature', async (request, response, directive: Directive, device: ScryptedDevice & ColorSettingTemperature) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.setColorTemperature(device.colorTemperature + 500);
commonColorTemperatureResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.ColorTemperatureController/DecreaseColorTemperature', async (request, response, directive: Directive, device: ScryptedDevice & ColorSettingTemperature) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.setColorTemperature(device.colorTemperature - 500);
commonColorTemperatureResponse(header, endpoint, payload, response, device);
});

View File

@@ -0,0 +1,71 @@
import { OnOff, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, ChangeReport, Report, Property, ChangePayload, DiscoveryCapability } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Outlet, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.OnOff))
return;
const capabilities: DiscoveryCapability[] = [];
capabilities.push({
"type": "AlexaInterface",
"interface": "Alexa.PowerController",
"version": "3",
"properties": {
"supported": [
{
"name": "powerState"
}
],
"proactivelyReported": true,
"retrievable": true
}
});
return {
displayCategories: ['SMARTPLUG'],
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & OnOff): Promise<Partial<Report>> {
return {
context: {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventSource.on ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
};
},
async sendEvent(eventSource: ScryptedDevice & OnOff, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.OnOff)
return undefined;
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": eventData ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
}
});

View File

@@ -10,7 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"**/plugin-console.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",

View File

@@ -39,6 +39,8 @@ Each 'Channel' or (camera) Device attached to the NVR must be configured as sepa
* `Snapshot URL Override` camera's IP address (preferred) or specific port number of NVR for that camera (may work). That is: `http://<camera IP address>/cgi-bin/snapshot.cgi` or `http://<NVR IP address>:<NVR port # for camera>/cgi-bin/snapshot.cgi`
* `Channel Number Override` camera's channel number as known to DVR
## Dahua Lock/Unlock
Dahua DTO video intercoms have built-in access control for locks/doors. If you have set the Amcrest plugin up with `Doorbell Type` set to `Dahua Doorbell`, you can enable support for remotely locking/unlocking by enabling/toggle the option `Enable Dahua Lock`.
# Troubleshooting
## General

View File

@@ -1,19 +1,21 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.132",
"version": "0.0.150",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.132",
"version": "0.0.150",
"license": "Apache",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk"
"@scrypted/sdk": "file:../../sdk",
"content-type": "^1.0.5"
},
"devDependencies": {
"@types/node": "^20.10.8"
"@types/content-type": "^1.1.8",
"@types/node": "^20.11.30"
}
},
"../../common": {
@@ -23,23 +25,22 @@
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^20.10.8",
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.29",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -77,15 +78,29 @@
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/content-type": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@types/content-type/-/content-type-1.1.8.tgz",
"integrity": "sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.10.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.8.tgz",
"integrity": "sha512-f8nQs3cLxbAFc00vEU59yf9UyGUftkPaLGfvbVOIDdx2i1b8epBqj2aNGyP19fiyXWvlmZ7qC1XLjAzw/OKIeA==",
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.132",
"version": "0.0.150",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -36,9 +36,11 @@
},
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk"
"@scrypted/sdk": "file:../../sdk",
"content-type": "^1.0.5"
},
"devDependencies": {
"@types/node": "^20.10.8"
"@types/content-type": "^1.1.8",
"@types/node": "^20.11.30"
}
}

View File

@@ -1,10 +1,140 @@
import { AuthFetchCredentialState, HttpFetchOptions, authHttpFetch } from '@scrypted/common/src/http-auth-fetch';
import { Readable } from 'stream';
import { readLine } from '@scrypted/common/src/read-stream';
import { parseHeaders, readBody } from '@scrypted/common/src/rtsp-server';
import contentType from 'content-type';
import { IncomingMessage } from 'http';
import { EventEmitter, Readable } from 'stream';
import { Destroyable } from '../../rtsp/src/rtsp';
import { getDeviceInfo } from './probe';
import { Point } from '@scrypted/sdk';
// Human
// {
// "Action" : "Cross",
// "Class" : "Normal",
// "CountInGroup" : 1,
// "DetectRegion" : [
// [ 455, 260 ],
// [ 3586, 260 ],
// [ 3768, 7580 ],
// [ 382, 7451 ]
// ],
// "Direction" : "Enter",
// "EventID" : 10181,
// "GroupID" : 0,
// "Name" : "Rule1",
// "Object" : {
// "Action" : "Appear",
// "BoundingBox" : [ 2856, 1280, 3880, 4880 ],
// "Center" : [ 3368, 3080 ],
// "Confidence" : 0,
// "LowerBodyColor" : [ 0, 0, 0, 0 ],
// "MainColor" : [ 0, 0, 0, 0 ],
// "ObjectID" : 863,
// "ObjectType" : "Human",
// "RelativeID" : 0,
// "Speed" : 0
// },
// "PTS" : 43380319830.0,
// "RuleID" : 2,
// "Track" : [],
// "UTC" : 1711446999,
// "UTCMS" : 701
// }
// Face
// {
// "CfgRuleId" : 1,
// "Class" : "FaceDetection",
// "CountInGroup" : 2,
// "DetectRegion" : null,
// "EventID" : 10360,
// "EventSeq" : 6,
// "Faces" : [
// {
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0
// }
// ],
// "FrameSequence" : 8251212,
// "GroupID" : 6,
// "Mark" : 0,
// "Name" : "FaceDetection",
// "Object" : {
// "Action" : "Appear",
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "Confidence" : 19,
// "FrameSequence" : 8251212,
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0,
// "SerialUUID" : "",
// "Source" : 0.0,
// "Speed" : 0,
// "SpeedTypeInternal" : 0
// },
// "Objects" : [
// {
// "Action" : "Appear",
// "BoundingBox" : [ 1504, 2336, 1728, 2704 ],
// "Center" : [ 1616, 2520 ],
// "Confidence" : 19,
// "FrameSequence" : 8251212,
// "ObjectID" : 94,
// "ObjectType" : "HumanFace",
// "RelativeID" : 0,
// "SerialUUID" : "",
// "Source" : 0.0,
// "Speed" : 0,
// "SpeedTypeInternal" : 0
// }
// ],
// "PTS" : 43774941350.0,
// "Priority" : 0,
// "RuleID" : 1,
// "RuleId" : 1,
// "Source" : -1280470024.0,
// "UTC" : 947510337,
// "UTCMS" : 0
// }
export interface AmcrestObjectDetails {
Action: string;
BoundingBox: Point;
Center: Point;
Confidence: number;
LowerBodyColor: [number, number, number, number];
MainColor: [number, number, number, number];
ObjectID: number;
ObjectType: string;
RelativeID: number;
Speed: number;
}
export interface AmcrestEventData {
Action: string;
Class: string;
CountInGroup: number;
DetectRegion: Point[];
Direction: string;
EventID: number;
GroupID: number;
Name: string;
Object: AmcrestObjectDetails;
PTS: number;
RuleID: number;
Track: any[];
UTC: number;
UTCMS: number;
}
export enum AmcrestEvent {
MotionStart = "Code=VideoMotion;action=Start",
MotionStop = "Code=VideoMotion;action=Stop",
MotionInfo = "Code=VideoMotionInfo;action=State",
AudioStart = "Code=AudioMutation;action=Start",
AudioStop = "Code=AudioMutation;action=Stop",
TalkInvite = "Code=_DoTalkAction_;action=Invite",
@@ -18,8 +148,33 @@ export enum AmcrestEvent {
DahuaTalkHangup = "Code=PassiveHungup;action=Start",
DahuaCallDeny = "Code=HungupPhone;action=Pulse",
DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse",
FaceDetection = "Code=FaceDetection;action=Start",
SmartMotionHuman = "Code=SmartMotionHuman;action=Start",
SmartMotionVehicle = "Code=Vehicle;action=Start",
CrossLineDetection = "Code=CrossLineDetection;action=Start",
CrossRegionDetection = "Code=CrossRegionDetection;action=Start",
}
async function readAmcrestMessage(client: Readable): Promise<string[]> {
let currentHeaders: string[] = [];
while (true) {
const originalLine = await readLine(client);
const line = originalLine.trim();
if (!line)
return currentHeaders;
// dahua bugs out and sends message without a newline separating the body:
// Content-Length:39
// Code=AudioMutation;action=Start;index=0
if (!line.includes(':')) {
client.unshift(Buffer.from(originalLine + '\n'));
return currentHeaders;
}
currentHeaders.push(line);
}
}
export class AmcrestCameraClient {
credential: AuthFetchCredentialState;
@@ -69,16 +224,17 @@ export class AmcrestCameraClient {
return getDeviceInfo(this.credential, this.ip);
}
async jpegSnapshot(): Promise<Buffer> {
async jpegSnapshot(timeout = 10000): Promise<Buffer> {
const response = await this.request({
url: `http://${this.ip}/cgi-bin/snapshot.cgi`,
timeout: 60000,
timeout,
});
return response.body;
}
async listenEvents() {
async listenEvents(): Promise<Destroyable> {
const events = new EventEmitter();
const url = `http://${this.ip}/cgi-bin/eventManager.cgi?action=attach&codes=[All]`;
console.log('preparing event listener', url);
@@ -86,32 +242,117 @@ export class AmcrestCameraClient {
url,
responseType: 'readable',
});
const stream = response.body;
const stream: IncomingMessage = response.body;
(events as any).destroy = () => {
stream.destroy();
events.removeAllListeners();
};
stream.on('close', () => {
events.emit('close');
});
stream.on('end', () => {
events.emit('end');
});
stream.on('error', e => {
events.emit('error', e);
});
stream.socket.setKeepAlive(true);
stream.on('data', (buffer: Buffer) => {
const data = buffer.toString();
const parts = data.split(';');
let index: string;
try {
for (const part of parts) {
if (part.startsWith('index')) {
index = part.split('=')[1]?.trim();
const ct = stream.headers['content-type'];
// make content type parsable as content disposition filename
const cd = contentType.parse(ct);
let { boundary } = cd.parameters;
boundary = `--${boundary}`;
const boundaryEnd = `${boundary}--`;
(async () => {
while (true) {
let ignore = await readLine(stream);
ignore = ignore.trim();
if (!ignore)
continue;
if (ignore === boundaryEnd)
continue;
// dahua bugs out and sends this.
if (ignore === 'HTTP/1.1 200 OK') {
const message = await readAmcrestMessage(stream);
this.console.log('ignoring dahua http message', message);
message.unshift('');
const headers = parseHeaders(message);
const body = await readBody(stream, headers);
if (body)
this.console.log('ignoring dahua http body', body);
continue;
}
if (ignore !== boundary) {
this.console.error('expected boundary but found', ignore);
this.console.error(response.headers);
throw new Error('expected boundary');
}
const message = await readAmcrestMessage(stream);
events.emit('data', message);
message.unshift('');
const headers = parseHeaders(message);
const body = await readBody(stream, headers);
const data = body.toString();
events.emit('data', data);
const parts = data.split(';');
let index: string;
try {
for (const part of parts) {
if (part.startsWith('index')) {
index = part.split('=')[1]?.trim();
}
}
}
catch (e) {
this.console.error('error parsing index', data);
}
let jsonData: any;
try {
for (const part of parts) {
if (part.startsWith('data')) {
jsonData = JSON.parse(part.split('=')[1]?.trim());
}
}
}
catch (e) {
this.console.error('error parsing data', data);
}
for (const event of Object.values(AmcrestEvent)) {
if (data.indexOf(event) !== -1) {
events.emit('event', event, index, data);
if (event === AmcrestEvent.SmartMotionHuman) {
events.emit('smart', 'person', jsonData);
}
else if (event === AmcrestEvent.SmartMotionVehicle) {
events.emit('smart', 'car', jsonData);
}
else if (event === AmcrestEvent.FaceDetection) {
events.emit('smart', 'face', jsonData);
}
else if (event === AmcrestEvent.CrossLineDetection || event === AmcrestEvent.CrossRegionDetection) {
const eventData: AmcrestEventData = jsonData;
if (eventData?.Object?.ObjectType === 'Human') {
events.emit('smart', 'person', eventData);
}
else if (eventData?.Object?.ObjectType === 'Vehicle') {
events.emit('smart', 'car', eventData);
}
}
}
}
}
catch (e) {
this.console.error('error parsing index', data);
}
// this.console?.log('event', data);
for (const event of Object.values(AmcrestEvent)) {
if (data.indexOf(event) !== -1) {
stream.emit('event', event, index, data);
}
}
});
return stream;
})()
.catch(() => stream.destroy());
return events as any as Destroyable;
}
async enableContinousRecording(channel: number) {
@@ -125,4 +366,20 @@ export class AmcrestCameraClient {
this.console.log(response.body);
}
}
async unlock(): Promise<boolean> {
const response = await this.request({
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=openDoor&channel=1&UserID=101&Type=Remote`,
responseType: 'text',
});
return response.body.includes('OK');
}
async lock(): Promise<boolean> {
const response = await this.request({
url: `http://${this.ip}/cgi-bin/accessControl.cgi?action=closeDoor&channel=1&UserID=101&Type=Remote`,
responseType: 'text',
});
return response.body.includes('OK');
}
}

View File

@@ -1,11 +1,11 @@
import { ffmpegLogInitialOutput } from '@scrypted/common/src/media-helpers';
import { readLength } from "@scrypted/common/src/read-stream";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, PictureOptions, Reboot, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, Lock, MediaObject, MediaStreamOptions, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, Reboot, RequestPictureOptions, RequestRecordingStreamOptions, ResponseMediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoCameraConfiguration, VideoRecorder } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable, Stream } from "stream";
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
import { AmcrestCameraClient, AmcrestEvent, AmcrestEventData } from "./amcrest-api";
const { mediaManager } = sdk;
@@ -22,12 +22,13 @@ function findValue(blob: string, prefix: string, key: string) {
return parts[1];
}
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, VideoRecorder, Reboot {
class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration, Camera, Intercom, Lock, VideoRecorder, Reboot, ObjectDetector {
eventStream: Stream;
cp: ChildProcess;
client: AmcrestCameraClient;
videoStreamOptions: Promise<UrlMediaStreamOptions[]>;
onvifIntercom = new OnvifIntercom(this);
hasSmartDetection: boolean;
constructor(nativeId: string, provider: RtspProvider) {
super(nativeId, provider);
@@ -36,6 +37,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
this.storage.removeItem('amcrestDoorbell');
}
this.hasSmartDetection = this.storage.getItem('hasSmartDetection') === 'true';
this.updateDevice();
this.updateDeviceInfo();
}
@@ -159,6 +161,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async listenEvents() {
let motionTimeout: NodeJS.Timeout;
const motionTimeoutDuration = 20000;
const resetMotionTimeout = () => {
clearTimeout(motionTimeout);
motionTimeout = setTimeout(() => {
this.motionDetected = false;
}, motionTimeoutDuration);
}
const client = new AmcrestCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console);
const events = await client.listenEvents();
const doorbellType = this.storage.getItem('doorbellType');
@@ -174,11 +186,21 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (idx.toString() !== channelNumber)
return;
}
if (event === AmcrestEvent.MotionStart) {
if (event === AmcrestEvent.MotionStart
|| event === AmcrestEvent.SmartMotionHuman
|| event === AmcrestEvent.SmartMotionVehicle
|| event === AmcrestEvent.CrossLineDetection
|| event === AmcrestEvent.CrossRegionDetection) {
this.motionDetected = true;
resetMotionTimeout();
}
else if (event === AmcrestEvent.MotionInfo) {
// this seems to be a motion pulse
if (this.motionDetected)
resetMotionTimeout();
}
else if (event === AmcrestEvent.MotionStop) {
this.motionDetected = false;
// use resetMotionTimeout
}
else if (event === AmcrestEvent.AudioStart) {
this.audioDetected = true;
@@ -220,9 +242,43 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
});
events.on('smart', (className: string, data: AmcrestEventData) => {
if (!this.hasSmartDetection) {
this.hasSmartDetection = true;
this.storage.setItem('hasSmartDetection', 'true');
this.updateDevice();
}
const detected: ObjectsDetected = {
timestamp: Date.now(),
detections: [
{
score: 1,
className,
}
],
};
this.onDeviceEvent(ScryptedInterface.ObjectDetector, detected);
});
return events;
}
async getDetectionInput(detectionId: string, eventId?: any): Promise<MediaObject> {
return;
}
async getObjectTypes(): Promise<ObjectDetectionTypes> {
return {
classes: [
'person',
'face',
'car',
],
}
}
async getOtherSettings(): Promise<Setting[]> {
const ret = await super.getOtherSettings();
ret.push(
@@ -259,6 +315,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (doorbellType == DAHUA_DOORBELL_TYPE) {
ret.push(
{
title: 'Enable Dahua Lock',
key: 'enableDahuaLock',
description: 'Some Dahua Doorbells have a built in lock/door access control.',
type: 'boolean',
value: (this.storage.getItem('enableDahuaLock') === 'true').toString(),
}
);
ret.push(
{
title: 'Multiple Call Buttons',
@@ -307,8 +373,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
async takeSmartCameraPicture(options?: RequestPictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getClient().jpegSnapshot(options?.timeout), 'image/jpeg');
}
async getUrlSettings() {
@@ -451,9 +517,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (isDoorbell || twoWayAudio) {
interfaces.push(ScryptedInterface.Intercom);
}
const enableDahuaLock = this.storage.getItem('enableDahuaLock') === 'true';
if (isDoorbell && doorbellType === DAHUA_DOORBELL_TYPE && enableDahuaLock) {
interfaces.push(ScryptedInterface.Lock);
}
const continuousRecording = this.storage.getItem('continuousRecording') === 'true';
if (continuousRecording)
interfaces.push(ScryptedInterface.VideoRecorder);
if (this.hasSmartDetection)
interfaces.push(ScryptedInterface.ObjectDetector);
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
}
@@ -496,7 +572,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
const doorbellType = this.storage.getItem('doorbellType');
// not sure if this all works, since i don't actually have a doorbell.
// good luck!
const channel = this.getRtspChannel() || '1';
@@ -523,12 +599,22 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
else {
args.push(
"-vn",
'-acodec', 'aac',
'-f', 'adts',
'pipe:3',
);
"-vn",
'-acodec', 'aac',
'-f', 'adts',
'pipe:3',
);
contentType = 'Audio/AAC';
// args.push(
// "-vn",
// '-acodec', 'pcm_mulaw',
// '-ac', '1',
// '-ar', '8000',
// '-sample_fmt', 's16',
// '-f', 'mulaw',
// 'pipe:3',
// );
// contentType = 'Audio/G.711A';
}
this.console.log('ffmpeg intercom', args);
@@ -548,15 +634,19 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
// seems the dahua doorbells preferred 1024 chunks. should investigate adts
// parsing and sending multipart chunks instead.
const passthrough = new PassThrough();
const abortController = new AbortController();
this.getClient().request({
url,
method: 'POST',
headers: {
'Content-Type': contentType,
'Content-Length': '9999999'
'Content-Length': '9999999',
},
signal: abortController.signal,
responseType: 'readable',
}, passthrough);
}, passthrough)
.catch(() => { })
.finally(() => this.console.log('request finished'))
try {
while (true) {
@@ -568,7 +658,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
finally {
this.console.log('audio finished');
passthrough.end();
passthrough.destroy();
abortController.abort();
}
this.stopIntercom();
@@ -587,6 +678,18 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
showRtspUrlOverride() {
return false;
}
async lock(): Promise<void> {
if (!this.client.lock()) {
this.console.error("Could not lock");
}
}
async unlock(): Promise<void> {
if (!this.client.unlock()) {
this.console.error("Could not unlock");
}
}
}
class AmcrestProvider extends RtspProvider {

View File

@@ -29,9 +29,14 @@ export async function getDeviceInfo(credential: AuthFetchCredentialState, addres
vals[k] = v.trim();
}
return {
const ret = {
deviceType: vals.deviceType,
hardwareVersion: vals.hardwareVersion,
serialNumber: vals.serialNumber,
}
};
if (!ret.deviceType && !ret.hardwareVersion && !ret.serialNumber)
throw new Error('not amcrest');
return ret;
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/bticino",
"version": "0.0.13",
"version": "0.0.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/bticino",
"version": "0.0.13",
"version": "0.0.15",
"dependencies": {
"@slyoldfox/sip": "^0.0.6-1",
"sdp": "^3.0.3",
@@ -30,23 +30,23 @@
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.2",
"version": "0.3.14",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -1219,10 +1219,10 @@
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"@types/node": "^16.9.0",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
},
"@scrypted/sdk": {
@@ -1232,7 +1232,7 @@
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/bticino",
"version": "0.0.13",
"version": "0.0.15",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",

View File

@@ -17,6 +17,12 @@ import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from
import { PersistentSipManager } from './persistent-sip-manager';
import { InviteHandler } from './bticino-inviteHandler';
import { SipOptions, SipRequest } from '../../sip/src/sip-manager';
import fs from "fs"
import url from "url"
import path from 'path';
import { default as stream } from 'node:stream'
import type { ReadableStream } from 'node:stream/web'
import { finished } from "stream/promises";
import { get } from 'http'
import { ControllerApi } from './c300x-controller-api';
@@ -25,6 +31,7 @@ import { BticinoMuteSwitch } from './bticino-mute-switch';
const STREAM_TIMEOUT = 65000;
const { mediaManager } = sdk;
const BTICINO_CLIPS = path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'bticino-clips');
export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor, DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips, Reboot {
@@ -147,11 +154,87 @@ export class BticinoSipCamera extends ScryptedDeviceBase implements MotionSensor
});
}
getVideoClip(videoId: string): Promise<MediaObject> {
let c300x = SipHelper.getIntercomIp(this)
const url = `http://${c300x}:8080/voicemail?msg=${videoId}/aswm.avi&raw=true`;
return mediaManager.createMediaObjectFromUrl(url);
async getVideoClip(videoId: string): Promise<MediaObject> {
const outputfile = await this.fetchAndConvertVoicemailMessage(videoId);
const fileURLToPath: string = url.pathToFileURL(outputfile).toString()
this.console.log(`Creating mediaObject for url: ${fileURLToPath}`)
return await mediaManager.createMediaObjectFromUrl(fileURLToPath);
}
private async fetchAndConvertVoicemailMessage(videoId: string) {
let c300x = SipHelper.getIntercomIp(this)
const response = await fetch(`http://${c300x}:8080/voicemail?msg=${videoId}/aswm.avi&raw=true`);
const contentLength: number = Number(response.headers.get("Content-Length"));
const lastModified: Date = new Date(response.headers.get("Last-Modified-Time"));
const avifile = `${BTICINO_CLIPS}/${videoId}.avi`;
const outputfile = `${BTICINO_CLIPS}/${videoId}.mp4`;
if (!fs.existsSync(BTICINO_CLIPS)) {
this.console.log(`Creating clips dir at: ${BTICINO_CLIPS}`)
fs.mkdirSync(BTICINO_CLIPS);
}
if (fs.existsSync(avifile)) {
const stat = fs.statSync(avifile);
if (stat.size != contentLength || stat.mtime.getTime() != lastModified.getTime()) {
this.console.log(`Size ${stat.size} != ${contentLength} or time ${stat.mtime.getTime} != ${lastModified.getTime}`)
try {
fs.rmSync(avifile);
} catch (e) { }
try {
fs.rmSync(outputfile);
} catch (e) { }
} else {
this.console.log(`Keeping the cached video at ${avifile}`)
}
}
if (!fs.existsSync(avifile)) {
this.console.log("Starting download.")
await finished(stream.Readable.from(response.body as ReadableStream<Uint8Array>).pipe(fs.createWriteStream(avifile)));
this.console.log("Download finished.")
try {
this.console.log(`Setting mtime to ${lastModified}`)
fs.utimesSync(avifile, lastModified, lastModified);
} catch (e) { }
}
const ffmpegPath = await mediaManager.getFFmpegPath();
const ffmpegArgs = [
'-hide_banner',
'-nostats',
'-y',
'-i', avifile,
outputfile
];
safePrintFFmpegArguments(console, ffmpegArgs);
const cp = child_process.spawn(ffmpegPath, ffmpegArgs, {
stdio: ['pipe', 'pipe', 'pipe', 'pipe'],
});
const p = new Promise((resolveFunc) => {
cp.stdout.on("data", (x) => {
this.console.log(x.toString());
});
cp.stderr.on("data", (x) => {
this.console.error(x.toString());
});
cp.on("exit", (code) => {
resolveFunc(code);
});
});
let returnCode = await p;
this.console.log(`Converted file returned code: ${returnCode}`);
return outputfile;
}
getVideoClipThumbnail(thumbnailId: string): Promise<MediaObject> {
let c300x = SipHelper.getIntercomIp(this)
const url = `http://${c300x}:8080/voicemail?msg=${thumbnailId}/aswm.jpg&raw=true`;

View File

@@ -33,8 +33,10 @@ export class VoicemailHandler extends SipRequestHandler {
handle(request: SipRequest) {
const lastVoicemailMessageTimestamp : number = Number.parseInt( this.sipCamera.storage.getItem('lastVoicemailMessageTimestamp') ) || -1
const message : string = request.content.toString()
if( message.startsWith('*#8**40*0*0*') || message.startsWith('*#8**40*1*0*') ) {
this.aswmIsEnabled = message.startsWith('*#8**40*1*0*');
let matches : Array<RegExpMatchArray> = [...message.matchAll(/\*#8\*\*40\*([01])\*([01])\*/gm)]
if( matches && matches.length > 0 && matches[0].length > 0 ) {
this.sipCamera.console.debug( "Answering machine state: " + matches[0][1] + " / Welcome message state: " + matches[0][2] );
this.aswmIsEnabled = matches[0][1] == '1';
if( this.isEnabled() ) {
this.sipCamera.console.debug("Handling incoming answering machine reply")
const messages : string[] = message.split(';')
@@ -60,6 +62,8 @@ export class VoicemailHandler extends SipRequestHandler {
this.sipCamera.console.debug("No new messages since: " + lastVoicemailMessageTimestamp + " lastMessage: " + lastMessageTimestamp)
}
}
} else {
this.sipCamera.console.debug("Not handling message: " + message)
}
}

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/chromecast",
"version": "0.1.56",
"version": "0.1.58",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/chromecast",
"version": "0.1.56",
"version": "0.1.58",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/chromecast",
"version": "0.1.56",
"version": "0.1.58",
"description": "Send video, audio, and text to speech notifications to Chromecast and Google Home devices",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -183,7 +183,7 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
media = await mediaManager.createMediaObjectFromUrl(media);
}
}
else if (options?.mimeType?.startsWith('image/')) {
else if (options?.mimeType?.startsWith('image/') || options?.mimeType?.startsWith('audio/')) {
url = await mediaManager.convertMediaObjectToInsecureLocalUrl(media, options?.mimeType);
}
@@ -215,7 +215,7 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
let cameraStreamAuthToken: string;
try {
cameraStreamAuthToken= await mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.LocalUrl);
cameraStreamAuthToken = await mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.LocalUrl);
}
catch (e) {
this.log.a('Streaming failed. Install and set up Scrypted Cloud to cast this media type.');
@@ -469,6 +469,12 @@ class CastDeviceProvider extends ScryptedDeviceBase implements DeviceProvider {
constructor() {
super(null);
endpointManager.setAccessControlAllowOrigin({
origins: [
// chromecast receiver
'https://koush.github.io',
],
});
this.browser.on('response', response => {
for (const additional of response.additionals) {
@@ -562,7 +568,7 @@ class CastDeviceProvider extends ScryptedDeviceBase implements DeviceProvider {
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
async discoverDevices(duration: number) {

View File

@@ -16,7 +16,7 @@
"sourceMaps": true,
"localRoot": "${workspaceFolder}/out",
"remoteRoot": "/plugin/",
"type": "pwa-node"
"type": "node"
}
]
}

View File

@@ -6,7 +6,8 @@
See below for additional recommendations.
## Port Forwarding
**Important Note**: Ports 10443 and 10444 are already being used by Scrypted itself. So, please choose a different port number, like 11443.
The network's router must configure an external port, the `From Port`, to the send traffic to the `Forward Port` on this server. These ports have random defaults that can be seen in the plugin Settings, and can be changed if preferred. Ports 10443 and 10444 are already being used by Scrypted itself, and should not be used. Choose another port, like 11443.
### What You'll Need
- Access to your router's settings (usually through a web browser).
@@ -18,24 +19,27 @@ See below for additional recommendations.
- For simplicity, use the same port number (e.g 11443) for both "From Port" and "Forward Port" fields in the Scrypted Cloud plugin settings General tab.
2. **Access Your Router Settings**
- Open your web browser and go to your router's login page. You may need the router's IP address, username, and password.
> If you're not sure how to do this, [find the guide specific to your router here](https://portforward.com/router.htm).
- Open your web browser and go to your router's login page. You may need the router's IP address, username, and password.
> If you're not sure how to do this, [find the guide specific to your router here](https://portforward.com/router.htm).
3. **Navigate to Firewall or Port Forwarding Section**
- Once logged in, find the section that deals with "Firewall" or "Port Forwarding". It could be under tabs like "Advanced," "NAT," or "Security."
- Once logged in, find the section that deals with "Firewall" or "Port Forwarding". It could be under tabs like "Advanced," "NAT," or "Security."
4. **Set Up Port Forwarding Rule**
- Use the port number you chose in Step 1 (e.g 11443) to set up a new Port Forwarding rule on your router.
- Use the port number you chose in Step 1 (e.g 11443) to set up a new Port Forwarding rule on your router.
5. **Change Port Forwarding Mode in Scrypted**
- Go back to Scrypted and navigate to the "General" tab in the Cloud plugin.
- Select "Router Forward" from the "Port Forwarding Mode" dropdown menu.
6. **Test Your Setup**
- In the Scrypted Cloud plugin settings, find and click the `Test Port Forward` button under the `Advanced` Settings tab. This will confirm if you've set everything up correctly.
- Go back to Scrypted and navigate to the "General" tab in the Cloud plugin.
- Select "Router Forward" from the "Port Forwarding Mode" dropdown menu.
7. **Save Your Settings**
- Don't forget to save your changes in both your router and in Scrypted.
6. **Save Your Settings**
- Don't forget to save your changes in both your router and in Scrypted.
7. **Reload Plugin**
- After all configuration is complete, Reload Cloud Plugin to ensure the new settings are applied.
6. **Test Your Setup**
- In the Scrypted Cloud plugin settings, find and click the `Test Port Forward` button under the `Advanced` Settings tab. This will confirm if everything is set up correctly.
### Firewall Configuration
Make sure your host machines firewall isn't blocking the port you've chosen. You may need to create an 'allow' rule for this port in your host's firewall settings.
@@ -46,7 +50,6 @@ Custom Domains can be used with the Cloud Plugin.
Set up a reverse proxy to the https Forward Port shown in settings.
## Cloudflare Tunnels
Scrypted Cloud automatically creates a login free tunnel for remote access.

File diff suppressed because it is too large Load Diff

View File

@@ -38,10 +38,9 @@
]
},
"dependencies": {
"@eneris/push-receiver": "^3.1.4",
"@eneris/push-receiver": "^3.1.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^1.4.0",
"bpmux": "^8.2.1",
"cloudflared": "^0.4.0",
"exponential-backoff": "^3.1.1",
@@ -49,10 +48,10 @@
"nat-upnp": "file:./external/node-nat-upnp"
},
"devDependencies": {
"@types/http-proxy": "^1.17.11",
"@types/ip": "^1.1.0",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^20.4.5"
"@types/http-proxy": "^1.17.14",
"@types/ip": "^1.1.3",
"@types/nat-upnp": "^1.1.5",
"@types/node": "^20.11.19"
},
"version": "0.2.4"
"version": "0.2.13"
}

View File

@@ -1,9 +1,12 @@
import { Deferred } from "@scrypted/common/src/deferred";
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import axios from 'axios';
import bpmux from 'bpmux';
import * as cloudflared from 'cloudflared';
import crypto from 'crypto';
import { once } from 'events';
import { backOff } from "exponential-backoff";
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
import http from 'http';
import HttpProxy from 'http-proxy';
import https from 'https';
@@ -11,24 +14,20 @@ import upnp from 'nat-upnp';
import net from 'net';
import os from 'os';
import path from 'path';
import { Duplex, Readable } from 'stream';
import { Duplex } from 'stream';
import tls from 'tls';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { PushManager } from './push';
import { readLine } from '../../../common/src/read-stream';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { httpFetch } from '../../../server/src/fetch/http-fetch';
import { PushManager } from './push';
import { qsparse, qsstringify } from "./qs";
import * as cloudflared from 'cloudflared';
import fs, { mkdirSync, renameSync, rmSync } from 'fs';
import { backOff } from "exponential-backoff";
import ip from 'ip';
import { Deferred } from "@scrypted/common/src/deferred";
// import { registerDuckDns } from "./greenlock";
const { deviceManager, endpointManager, systemManager } = sdk;
export const DEFAULT_SENDER_ID = '827888101440';
const SCRYPTED_SERVER = 'home.scrypted.app';
const SCRYPTED_SERVER = localStorage.getItem('scrypted-server') || 'home.scrypted.app';
const SCRYPTED_CLOUD_MESSAGE_PATH = '/_punch/cloudmessage';
@@ -69,11 +68,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
},
registrationSecret: {
hide: true,
persistedDefaultValue: crypto.randomBytes(8).toString('base64'),
},
cloudMessageToken: {
hide: true,
persistedDefaultValue: crypto.randomBytes(8).toString('hex'),
},
serverId: {
hide: true,
persistedDefaultValue: crypto.randomBytes(8).toString('hex'),
},
forwardingMode: {
title: "Port Forwarding Mode",
description: "The port forwarding mode used to expose the HTTPS port. If port forwarding is disabled or unavailable, Scrypted Cloud will fall back to push to initiate connections with this Scrypted server. Port Forwarding and UPNP are optional but will significantly speed up cloud connections.",
@@ -152,7 +156,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
json: true,
},
cloudflareEnabled: {
group: 'Advanced',
group: 'Cloudflare',
title: 'Cloudflare',
type: 'boolean',
description: 'Optional: Create a Cloudflare Tunnel to this server at a random domain name. Providing a Cloudflare token will allow usage of a custom domain name.',
@@ -160,7 +164,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
onPut: () => deviceManager.requestRestart(),
},
cloudflaredTunnelToken: {
group: 'Advanced',
group: 'Cloudflare',
title: 'Cloudflare Tunnel Token',
description: 'Optional: Enter the Cloudflare token from the Cloudflare Dashbaord to track and manage the tunnel remotely.',
onPut: () => {
@@ -168,14 +172,27 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
},
},
cloudflaredTunnelUrl: {
group: 'Advanced',
group: 'Cloudflare',
title: 'Cloudflare Tunnel URL',
description: 'Cloudflare Tunnel URL is a randomized cloud connection, unless a Cloudflare Tunnel Token is provided.',
readonly: true,
mapGet: () => this.cloudflareTunnel || 'Unavailable',
},
serverName: {
group: 'Connection',
title: 'Server Name',
description: 'The name of this server. This is used to identify this server in the Scrypted Cloud.',
persistedDefaultValue: os.hostname()?.split('.')[0] || 'Scrypted Server',
},
connectHomeScryptedApp: {
group: 'Connection',
title: `Connect to ${SCRYPTED_SERVER}`,
description: `Connect this server to ${SCRYPTED_SERVER}. This is required to use the Scrypted Cloud.`,
type: 'boolean',
persistedDefaultValue: true,
},
register: {
group: 'Advanced',
group: 'Connection',
title: 'Register',
type: 'button',
onPut: () => {
@@ -184,7 +201,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
description: 'Register server with Scrypted Cloud.',
},
testPortForward: {
group: 'Advanced',
group: 'Connection',
title: 'Test Port Forward',
type: 'button',
onPut: () => this.testPortForward(),
@@ -243,7 +260,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.storageSettings.settings.securePort.onGet = async () => {
return {
group: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Advanced' : undefined,
group: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare' : undefined,
title: this.storageSettings.values.forwardingMode === 'Disabled' ? 'Cloudflare Port' : 'Forward Port',
}
};
@@ -288,25 +305,35 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if (!this.storageSettings.values.certificate)
this.storageSettings.values.certificate = createSelfSignedCertificate();
this.setupProxyServer();
const proxy = this.setupProxyServer();
this.setupCloudPush();
this.manager.on('registrationId', async (registrationId) => {
// currently the fcm registration id never changes, so, there's no need.
// if ever adding clockwork push, uncomment this.
this.sendRegistrationId(registrationId);
});
this.manager.registrationId.then(async registrationId => {
if (this.storageSettings.values.lastPersistedRegistrationId !== registrationId || !this.storageSettings.values.registrationSecret)
this.sendRegistrationId(registrationId);
})
this.updateCors();
const observeRegistrations = () => {
this.manager.on('registrationId', async (registrationId) => {
// currently the fcm registration id never changes, so, there's no need.
// if ever adding clockwork push, uncomment this.
this.sendRegistrationId(registrationId);
});
this.upnpInterval = setInterval(() => this.refreshPortForward(), 30 * 60 * 1000);
this.refreshPortForward();
}
if (!this.storageSettings.values.token_info && process.env.SCRYPTED_CLOUD_TOKEN) {
this.storageSettings.values.token_info = process.env.SCRYPTED_CLOUD_TOKEN;
this.manager.registrationId.then(r => this.sendRegistrationId(r));
this.manager.registrationId.then(r => {
this.sendRegistrationId(r, true);
proxy.then(observeRegistrations);
});
}
else {
this.manager.registrationId.then(async registrationId => {
if (this.storageSettings.values.lastPersistedRegistrationId !== registrationId)
this.sendRegistrationId(registrationId);
});
proxy.then(observeRegistrations);
}
}
@@ -334,7 +361,9 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
const url = new URL('https://www.duckdns.org/update');
url.searchParams.set('domains', this.storageSettings.values.duckDnsHostname);
url.searchParams.set('token', this.storageSettings.values.duckDnsToken);
await axios(url.toString());
await httpFetch({
url: url.toString(),
});
}
catch (e) {
this.console.error('Duck DNS Erorr', e);
@@ -361,11 +390,16 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
ip = this.storageSettings.values.duckDnsHostname;
}
else if (this.cloudflareTunnelHost) {
ip = this.cloudflareTunnelHost;
}
else {
ip = (await axios(`https://${SCRYPTED_SERVER}/_punch/ip`)).data.ip;
if (!this.cloudflareTunnelHost) {
ip = (await httpFetch({
url: `https://${SCRYPTED_SERVER}/_punch/ip`,
responseType: 'json',
})).body.ip;
}
if (this.cloudflareTunnelHost)
ip = this.cloudflareTunnelHost
}
if (this.storageSettings.values.forwardingMode === 'Custom Domain' || this.cloudflareTunnelHost)
@@ -379,6 +413,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
const registrationId = await this.manager.registrationId;
const data = await this.sendRegistrationId(registrationId);
if (data?.error)
return;
if (ip !== 'localhost' && ip !== data.ip_address && ip !== this.cloudflareTunnelHost) {
this.log.a(`Scrypted Cloud could not verify the IP Address of your custom domain ${this.storageSettings.values.hostname}.`);
}
@@ -400,11 +436,14 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
if (!hostname)
hostname = 'localhost';
url.searchParams.set('url', `https://${hostname}:${upnp_port}${pluginPath}/testPortForward`);
const response = await axios(url.toString());
this.console.log('test data:', response.data);
if (response.data.error)
throw new Error(response.data.error);
if (response.data.data !== this.randomBytes)
const response = await httpFetch({
url: url.toString(),
responseType: 'json',
});
this.console.log('test data:', response.body);
if (response.body.error)
throw new Error(response.body.error);
if (response.body.data !== this.randomBytes)
throw new Error('Server received data that did not match this server.');
this.log.a("Port Forward Test Succeeded.");
}
@@ -418,7 +457,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
let { upnpPort } = this.storageSettings.values;
if (!upnpPort)
upnpPort = Math.round(Math.random() * 30000 + 20000);
upnpPort = Math.round(Math.random() * 20000 + 40000);
if (this.storageSettings.values.forwardingMode === 'Disabled') {
this.updatePortForward(upnpPort);
@@ -494,17 +533,18 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
scope: local.pathname,
ttl,
})
const scope = await axios(`https://${this.getHostname()}/_punch/scope?${q}`, {
const scope = await httpFetch({
url: `https://${this.getHostname()}/_punch/scope?${q}`,
headers: {
Authorization: `Bearer ${token_info}`
},
responseType: 'json',
})
const { userToken, userTokenSignature } = scope.data;
const { userToken, userTokenSignature } = scope.body;
const tokens = qsstringify({
user_token: userToken,
user_token_signature: userTokenSignature
})
});
const url = `${baseUrl}${local.pathname}?${tokens}`;
this.whitelisted.set(local.pathname, url);
@@ -517,8 +557,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
origins: [
`http://${SCRYPTED_SERVER}`,
`https://${SCRYPTED_SERVER}`,
// chromecast receiver. move this into google home and chromecast plugins?
'https://koush.github.io',
...this.storageSettings.values.additionalCorsOrigins,
],
});
@@ -542,50 +580,89 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
getAuthority() {
const upnp_port = this.storageSettings.values.forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
const hostname = this.storageSettings.values.forwardingMode === 'Custom Domain'
const { forwardingMode } = this.storageSettings.values;
if (forwardingMode === 'Disabled')
return {};
const upnp_port = forwardingMode === 'Custom Domain' ? 443 : this.storageSettings.values.upnpPort;
const hostname = forwardingMode === 'Custom Domain'
? this.storageSettings.values.hostname
: this.storageSettings.values.duckDnsToken && this.storageSettings.values.duckDnsHostname;
if (upnp_port === 443 && !hostname) {
const error = this.storageSettings.values.forwardingMode === 'Custom Domain'
const error = forwardingMode === 'Custom Domain'
? 'Hostname is required for port Custom Domain setup.'
: 'Port 443 requires Custom Domain configuration.';
this.log.a(error);
throw new Error(error);
}
if (!hostname) {
return {
upnp_port,
port: upnp_port,
};
}
return {
upnp_port,
port: upnp_port,
hostname,
}
}
async sendRegistrationId(registration_id: string) {
const { upnp_port, hostname } = this.getAuthority();
const registration_secret = this.storageSettings.values.registrationSecret || crypto.randomBytes(8).toString('base64');
async sendRegistrationId(registration_id: string, force?: boolean) {
const authority = this.getAuthority();
const q = qsstringify({
upnp_port,
...authority,
registration_id,
server_id: this.storageSettings.values.serverId,
server_name: this.storageSettings.values.serverName,
sender_id: DEFAULT_SENDER_ID,
registration_secret,
hostname,
registration_secret: this.storageSettings.values.registrationSecret,
force: force ? 'true' : '',
});
if (!this.storageSettings.values.connectHomeScryptedApp) {
return {
error: `Scrypted Cloud connection to ${SCRYPTED_SERVER} is disabled.`,
};
}
const { token_info } = this.storageSettings.values;
if (!token_info)
throw new Error('Scrypted Cloud is not logged in. Skipping home.scrypted.app registration.');
const response = await axios(`https://${SCRYPTED_SERVER}/_punch/register?${q}`, {
headers: {
Authorization: `Bearer ${token_info}`
},
});
this.console.log('registered', response.data);
this.storageSettings.values.lastPersistedRegistrationId = registration_id;
this.storageSettings.values.lastPersistedUpnpPort = upnp_port;
this.storageSettings.values.registrationSecret = registration_secret;
return response.data;
if (!token_info) {
const error = `Login to the Scrypted Cloud plugin to reach this server from the cloud, or disable this alert in the Scrypted Cloud plugin Connection settings.`;
this.log.a(error);
return {
error,
};
}
try {
const response = await httpFetch({
url: `https://${SCRYPTED_SERVER}/_punch/register?${q}`,
headers: {
Authorization: `Bearer ${token_info}`
},
responseType: 'json',
})
const error = response.body?.error;
if (error) {
this.console.log('registration error', response.body);
this.log.a(error);
return response.body;
}
this.console.log('registered', response.body);
this.storageSettings.values.lastPersistedRegistrationId = registration_id;
this.storageSettings.values.lastPersistedUpnpPort = authority.upnp_port;
return response.body;
}
catch (e) {
return {
error: e.toString(),
};
}
}
async setupCloudPush() {
@@ -644,7 +721,18 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
return this.getSSLHostname() || SCRYPTED_SERVER;
}
async convert(data: Buffer, fromMimeType: string): Promise<Buffer> {
async convert(data: Buffer, fromMimeType: string, toMimeType: string): Promise<Buffer> {
// if cloudflare is enabled and the plugin isn't set up as a custom domain, try to use the cloudflare url for
// short lived urls.
if (this.cloudflareTunnel && this.storageSettings.values.forwardingMode !== 'Custom Domain') {
const params = new URLSearchParams(toMimeType.split(';')[1] || '');
if (params.get('short-lived') === 'true') {
const u = new URL(data.toString(), this.cloudflareTunnel);
u.host = this.cloudflareTunnelHost;
u.port = '';
return Buffer.from(u.toString());
}
}
return this.whitelist(data.toString(), 10 * 365 * 24 * 60 * 60 * 1000, `https://${this.getHostname()}`);
}
@@ -677,9 +765,15 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
async getOauthUrl(): Promise<string> {
const authority = this.getAuthority();
const args = qsstringify({
hostname: os.hostname(),
...authority,
registration_id: await this.manager.registrationId,
registration_secret: this.storageSettings.values.registrationSecret,
server_id: this.storageSettings.values.serverId,
server_name: this.storageSettings.values.serverName,
sender_id: DEFAULT_SENDER_ID,
redirect_uri: `https://${SCRYPTED_SERVER}/web/oauth/callback`,
})
@@ -783,9 +877,6 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
await once(this.secureServer, 'listening');
this.storageSettings.values.securePort = this.securePort = (this.secureServer.address() as any).port;
this.upnpInterval = setInterval(() => this.refreshPortForward(), 30 * 60 * 1000);
this.refreshPortForward();
const agent = new http.Agent({ maxSockets: Number.MAX_VALUE, keepAlive: true });
this.proxy = HttpProxy.createProxy({
agent,
@@ -824,16 +915,20 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.console.log('scrypted server requested a connection:', random);
const registrationId = await this.manager.registrationId;
this.ensureReverseConnections(registrationId);
const client = tls.connect(4001, SCRYPTED_SERVER, {
const { address } = message;
const [serverHost, serverPort] = address?.split(':') || [SCRYPTED_SERVER, 4001];
this.ensureReverseConnections(registrationId, serverPort, serverHost);
const client = tls.connect(serverPort, serverHost, {
rejectUnauthorized: false,
});
client.on('close', () => this.console.log('scrypted server connection ended:', random));
client.write(registrationId + '\n');
const mux: any = new bpmux.BPMux(client as any);
mux.on('handshake', async (socket: Duplex) => {
this.ensureReverseConnections(registrationId);
this.ensureReverseConnections(registrationId, serverPort, serverHost);
this.console.warn('mux connection required');
@@ -885,10 +980,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
});
const tmp = `${bin}.tmp`;
const stream = await axios('https://github.com/scryptedapp/cloudflared/releases/download/2023.8.2/cloudflared-darwin-arm64', {
responseType: 'stream',
const stream = await httpFetch({
url: 'https://github.com/scryptedapp/cloudflared/releases/download/2023.8.2/cloudflared-darwin-arm64',
responseType: 'readable',
});
const write = stream.data.pipe(fs.createWriteStream(tmp));
const write = stream.body.pipe(fs.createWriteStream(tmp));
await once(write, 'close');
renameSync(tmp, bin);
fs.chmodSync(bin, 0o0755)
@@ -976,14 +1072,14 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
}
}
ensureReverseConnections(registrationId: string) {
ensureReverseConnections(registrationId: string, serverPort: number, serverHost: string) {
while (this.reverseConnections.size < 10) {
this.createReverseConnection(registrationId);
this.createReverseConnection(registrationId, serverPort, serverHost);
}
}
async createReverseConnection(registrationId: string) {
const client = tls.connect(4001, SCRYPTED_SERVER, {
async createReverseConnection(registrationId: string, serverPort: number, serverHost: string) {
const client = tls.connect(serverPort, serverHost, {
rejectUnauthorized: false,
});
this.reverseConnections.add(client);
@@ -994,7 +1090,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
this.reverseConnections.delete(client);
if (claimed)
this.ensureReverseConnections(registrationId);
this.ensureReverseConnections(registrationId, serverPort, serverHost);
});
client.write(`reverse:${registrationId}\n`);

View File

@@ -0,0 +1,20 @@
[Unit]
Description=Scrypted service
After=network.target
[Service]
User=root
Group=root
Type=simple
ExecStart=/usr/bin/npx -y scrypted serve
Restart=always
RestartSec=3
Environment="NODE_OPTIONS=--dns-result-order=ipv4first"
Environment="SCRYPTED_PYTHON_PATH=/usr/bin/python3"
Environment="SCRYPTED_PYTHON39_PATH=/usr/bin/python3.9"
Environment="SCRYPTED_PYTHON310_PATH=/usr/bin/python3.10"
Environment="SCRYPTED_FFMPEG_PATH=/usr/bin/ffmpeg"
Environment="SCRYPTED_INSTALL_ENVIRONMENT=lxc"
[Install]
WantedBy=multi-user.target

View File

@@ -1,24 +1,25 @@
{
"name": "@scrypted/core",
"version": "0.3.3",
"version": "0.3.23",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/core",
"version": "0.3.3",
"version": "0.3.23",
"license": "Apache-2.0",
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/node-pty": "^1.0.5",
"@scrypted/sdk": "file:../../sdk",
"mime": "^3.0.0",
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
"router": "^1.3.8",
"typescript": "^5.2.2"
"typescript": "^5.4.2"
},
"devDependencies": {
"@types/mime": "^3.0.4",
"@types/node": "^20.9.2"
"@types/node": "^20.11.26"
}
},
"../../../sdk": {
@@ -78,22 +79,22 @@
"dependencies": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"http-auth-utils": "^5.0.1",
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@types/node": "^20.11.0",
"ts-node": "^10.9.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.3.4",
"version": "0.3.18",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -127,6 +128,122 @@
"resolved": "../../common",
"link": true
},
"node_modules/@scrypted/node-pty": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@scrypted/node-pty/-/node-pty-1.0.5.tgz",
"integrity": "sha512-C3Q7mcrLq8irKC34hgMk6cpUdnJh4LHQ4pnUjVJvsZ5zIRO1G4Z7HZHqUCAn7wGfHmBSmVAbNR1ZL6KvXSRacQ==",
"hasInstallScript": true,
"dependencies": {
"prebuild-install": "^7.1.2"
}
},
"node_modules/@scrypted/node-pty/node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@scrypted/node-pty/node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"engines": {
"node": ">=8"
}
},
"node_modules/@scrypted/node-pty/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@scrypted/node-pty/node_modules/node-abi": {
"version": "3.56.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz",
"integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@scrypted/node-pty/node_modules/prebuild-install": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
"integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@scrypted/node-pty/node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@scrypted/node-pty/node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
@@ -138,9 +255,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
"integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
"version": "20.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
"integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -387,6 +504,17 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -432,8 +560,8 @@
},
"node_modules/nan": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w=="
"resolved": "git+ssh://git@github.com/ajgassner/nan.git#f4933dedce0fb160927ffe5d7896b33ef461f17c",
"license": "MIT"
},
"node_modules/napi-build-utils": {
"version": "1.0.2",
@@ -752,9 +880,9 @@
}
},
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -794,6 +922,11 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
},
"dependencies": {
@@ -802,10 +935,83 @@
"requires": {
"@scrypted/sdk": "file:../sdk",
"@scrypted/server": "file:../server",
"@types/node": "^16.9.0",
"http-auth-utils": "^3.0.2",
"node-fetch-commonjs": "^3.1.1",
"typescript": "^4.4.3"
"@types/node": "^20.11.0",
"http-auth-utils": "^5.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
},
"@scrypted/node-pty": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@scrypted/node-pty/-/node-pty-1.0.5.tgz",
"integrity": "sha512-C3Q7mcrLq8irKC34hgMk6cpUdnJh4LHQ4pnUjVJvsZ5zIRO1G4Z7HZHqUCAn7wGfHmBSmVAbNR1ZL6KvXSRacQ==",
"requires": {
"prebuild-install": "^7.1.2"
},
"dependencies": {
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"requires": {
"mimic-response": "^3.1.0"
}
},
"detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="
},
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
},
"node-abi": {
"version": "3.56.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz",
"integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==",
"requires": {
"semver": "^7.3.5"
}
},
"prebuild-install": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
"integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
"requires": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
}
},
"semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"requires": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
}
}
},
"@scrypted/sdk": {
@@ -815,7 +1021,7 @@
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"axios": "^1.6.5",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
@@ -839,9 +1045,9 @@
"dev": true
},
"@types/node": {
"version": "20.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.2.tgz",
"integrity": "sha512-WHZXKFCEyIUJzAwh3NyyTHYSR35SevJ6mZ1nWwJafKtiQbqRTIKSRcw3Ma3acqgsent3RRDqeVwpHntMk+9irg==",
"version": "20.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
"integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
@@ -1021,6 +1227,14 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
@@ -1047,9 +1261,8 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"nan": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w=="
"version": "git+ssh://git@github.com/ajgassner/nan.git#f4933dedce0fb160927ffe5d7896b33ef461f17c",
"from": "nan@^2.14.2"
},
"napi-build-utils": {
"version": "1.0.2",
@@ -1315,9 +1528,9 @@
}
},
"typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w=="
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ=="
},
"undici-types": {
"version": "5.26.5",
@@ -1347,6 +1560,11 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.3.3",
"version": "0.3.23",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",
@@ -28,7 +28,6 @@
"interfaces": [
"@scrypted/launcher-ignore",
"HttpRequestHandler",
"EngineIOHandler",
"DeviceProvider",
"SystemSettings",
"Settings"
@@ -40,14 +39,15 @@
},
"dependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/node-pty": "^1.0.5",
"@scrypted/sdk": "file:../../sdk",
"mime": "^3.0.0",
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
"router": "^1.3.8",
"typescript": "^5.2.2"
"typescript": "^5.4.2"
},
"devDependencies": {
"@types/mime": "^3.0.4",
"@types/node": "^20.9.2"
"@types/node": "^20.11.26"
}
}

View File

@@ -26,18 +26,18 @@ export class Scheduler {
schedule.saturday,
];
const date = new Date();
date.setHours(schedule.hour);
date.setMinutes(schedule.minute);
const ret: ScryptedDevice = {
async setName() { },
async setType() { },
async setRoom() { },
async setMixins() { },
async probe() { return true },
async probe() { return true; },
listen(event: EventListenerOptions, callback, source?: ScryptedDeviceBase) {
function reschedule(): Date {
const date = new Date();
date.setHours(schedule.hour);
date.setMinutes(schedule.minute);
const now = Date.now();
for (let i = 0; i < 8; i++) {
const future = new Date(date.getTime() + i * 24 * 60 * 60 * 1000);
@@ -65,7 +65,7 @@ export class Scheduler {
eventId: undefined,
eventInterface: 'Scheduler',
eventTime: Date.now(),
}, prevWhen)
}, prevWhen);
}
function setupTimer() {
@@ -87,8 +87,13 @@ export class Scheduler {
timeout = null;
when = null;
}
}
}
};
},
id: "",
pluginId: "",
interfaces: [],
mixins: [],
providedInterfaces: []
}
ret.name = 'Scheduler';

View File

@@ -1,24 +1,24 @@
import { readFileAsString, tsCompile } from '@scrypted/common/src/eval/scrypted-eval';
import sdk, { DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk';
import sdk, { DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
import { StorageSettings } from "@scrypted/sdk/storage-settings";
import fs from 'fs';
import net from 'net';
import os from 'os';
import Router from 'router';
import { AggregateCore, AggregateCoreNativeId } from './aggregate-core';
import { AutomationCore, AutomationCoreNativeId } from './automations-core';
import { LauncherMixin } from './launcher-mixin';
import { MediaCore } from './media-core';
import { newScript, ScriptCore, ScriptCoreNativeId } from './script-core';
import { ConsoleServiceNativeId, PluginSocketService, ReplServiceNativeId } from './plugin-socket-service';
import { ScriptCore, ScriptCoreNativeId, newScript } from './script-core';
import { TerminalService, TerminalServiceNativeId } from './terminal-service';
import { UsersCore, UsersNativeId } from './user';
import { checkLxcDependencies } from './platform/lxc';
const { systemManager, deviceManager, endpointManager } = sdk;
export function getAddresses() {
const addresses: string[] = [];
for (const [iface, nif] of Object.entries(os.networkInterfaces())) {
if (iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')) {
if (iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan') || iface.startsWith('net')) {
addresses.push(iface);
addresses.push(...nif.map(addr => addr.address));
}
@@ -30,7 +30,7 @@ interface RoutedHttpRequest extends HttpRequest {
params: { [key: string]: string };
}
class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, EngineIOHandler, DeviceProvider, Settings {
class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, DeviceProvider, Settings {
router: any = Router();
publicRouter: any = Router();
mediaCore: MediaCore;
@@ -38,6 +38,8 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
aggregateCore: AggregateCore;
automationCore: AutomationCore;
users: UsersCore;
consoleService: PluginSocketService;
replService: PluginSocketService;
terminalService: TerminalService;
localAddresses: string[];
storageSettings = new StorageSettings(this, {
@@ -64,6 +66,8 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
constructor() {
super();
checkLxcDependencies();
this.indexHtml = readFileAsString('dist/index.html');
(async () => {
@@ -96,6 +100,26 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
},
);
})();
(async () => {
await deviceManager.onDeviceDiscovered(
{
name: 'REPL Service',
nativeId: ReplServiceNativeId,
interfaces: [ScryptedInterface.StreamService],
type: ScryptedDeviceType.Builtin,
},
);
})();
(async () => {
await deviceManager.onDeviceDiscovered(
{
name: 'Console Service',
nativeId: ConsoleServiceNativeId,
interfaces: [ScryptedInterface.StreamService],
type: ScryptedDeviceType.Builtin,
},
);
})();
(async () => {
await deviceManager.onDeviceDiscovered(
@@ -172,47 +196,15 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
return this.users ||= new UsersCore();
if (nativeId === TerminalServiceNativeId)
return this.terminalService ||= new TerminalService();
if (nativeId === ReplServiceNativeId)
return this.replService ||= new PluginSocketService(ReplServiceNativeId, 'repl');
if (nativeId === ConsoleServiceNativeId)
return this.consoleService ||= new PluginSocketService(ConsoleServiceNativeId, 'console');
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
checkEngineIoEndpoint(request: HttpRequest, name: string) {
const check = `/endpoint/@scrypted/core/engine.io/${name}/`;
if (!request.url.startsWith(check))
return null;
return check;
}
async checkService(request: HttpRequest, ws: WebSocket, name: string): Promise<boolean> {
// only allow admin users to access these services.
if (request.aclId)
return false;
const check = this.checkEngineIoEndpoint(request, name);
if (!check)
return false;
const deviceId = request.url.substr(check.length).split('/')[0];
const plugins = await systemManager.getComponent('plugins');
const { nativeId, pluginId } = await plugins.getDeviceInfo(deviceId);
const port = await plugins.getRemoteServicePort(pluginId, name);
const socket = net.connect(port);
socket.on('close', () => ws.close());
socket.on('data', data => ws.send(data));
socket.resume();
socket.write(nativeId?.toString() || 'undefined');
ws.onclose = () => socket.destroy();
ws.onmessage = message => socket.write(message.data);
return true;
}
async onConnection(request: HttpRequest, ws: WebSocket): Promise<void> {
if (await this.checkService(request, ws, 'console') || await this.checkService(request, ws, 'repl')) {
return;
}
ws.close();
}
async handlePublicFinal(request: HttpRequest, response: HttpResponse) {
// need to strip off the query.
const incomingPathname = request.url.split('?')[0];

View File

@@ -0,0 +1,46 @@
import fs from 'fs';
import child_process from 'child_process';
import { once } from 'events';
import sdk from '@scrypted/sdk';
export const SCRYPTED_INSTALL_ENVIRONMENT_LXC = 'lxc';
export async function checkLxcDependencies() {
if (process.env.SCRYPTED_INSTALL_ENVIRONMENT !== SCRYPTED_INSTALL_ENVIRONMENT_LXC)
return;
let needRestart = false;
if (!process.version.startsWith('v20.')) {
const cp = child_process.spawn('sh', ['-c', 'apt update -y && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt install -y nodejs']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install Node.js 20.x.');
else
needRestart = true;
}
if (!fs.existsSync('/var/run/avahi-daemon/socket')) {
const cp = child_process.spawn('sh', ['-c', 'apt update -y && apt install -y avahi-daemon && apt upgrade -y']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to install avahi-daemon.');
else
needRestart = true;
}
const scryptedService = fs.readFileSync('lxc/scrypted.service').toString();
const installedScryptedService = fs.readFileSync('/etc/systemd/system/scrypted.service').toString();
if (installedScryptedService !== scryptedService) {
fs.writeFileSync('/etc/systemd/system/scrypted.service', scryptedService);
needRestart = true;
const cp = child_process.spawn('systemctl', ['daemon-reload']);
const [exitCode] = await once(cp, 'exit');
if (exitCode !== 0)
sdk.log.a('Failed to daemon-reload systemd.');
}
if (needRestart)
sdk.log.a('A system update is pending. Please restart Scrypted to apply changes.');
}

View File

@@ -0,0 +1,82 @@
import { createAsyncQueue } from '@scrypted/common/src/async-queue';
import sdk, { ScryptedDeviceBase, ScryptedNativeId, StreamService } from "@scrypted/sdk";
import { once } from 'events';
import net from 'net';
export const ReplServiceNativeId = 'replservice';
export const ConsoleServiceNativeId = 'consoleservice';
export class PluginSocketService extends ScryptedDeviceBase implements StreamService {
constructor(nativeId: ScryptedNativeId, public serviceName: string) {
super(nativeId);
}
async connectStream(input?: AsyncGenerator<Buffer | string, void>, options?: any): Promise<AsyncGenerator<Buffer, void>> {
const pluginId = options?.pluginId as string;
if (!pluginId)
throw new Error('must provide pluginId');
const plugins = await sdk.systemManager.getComponent('plugins');
const replPort: number = await plugins.getRemoteServicePort(pluginId, this.serviceName);
const socket = net.connect(replPort);
await once(socket, 'connect');
const queue = createAsyncQueue<Buffer>();
socket.on('close', () => queue.end());
socket.on('end', () => queue.end());
let bufferedLength = 0;
const MAX_BUFFERED_LENGTH = 64000;
socket.on('data', async data => {
const buffer = Buffer.from(data);
bufferedLength += buffer.length;
const promise = queue.enqueue(buffer).then(() => bufferedLength -= buffer.length);
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
socket.pause();
await promise;
if (bufferedLength < MAX_BUFFERED_LENGTH)
socket.resume();
}
});
async function* generator() {
try {
while (true) {
const buffers = queue.clear();
if (buffers.length) {
yield Buffer.concat(buffers);
continue;
}
yield await queue.dequeue();
}
}
catch (e) {
}
finally {
socket.destroy();
}
}
(async () => {
try {
for await (const message of input) {
if (!message)
continue;
if (!Buffer.isBuffer(message))
throw new Error("unexpected control message");
socket.write(message);
}
}
catch (e) {
this.console.log(e);
}
socket.destroy();
})();
return generator();
}
}

View File

@@ -115,7 +115,7 @@ export class ScriptCore extends ScryptedDeviceBase implements DeviceProvider, De
}
catch (e) {
worker.terminate();
throw e;
// throw e;
}
}

View File

@@ -172,8 +172,16 @@ export class TerminalService extends ScryptedDeviceBase implements StreamService
cp?.sendEOF();
} else if ("interactive" in parsed && !cp) {
if (parsed.interactive) {
let spawn: typeof ptySpawn;
try {
const spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
try {
spawn = require('node-pty-prebuilt-multiarch').spawn as typeof ptySpawn;
if (!spawn)
throw new Error();
}
catch (e) {
spawn = require('@scrypted/node-pty').spawn as typeof ptySpawn;
}
cp = new InteractiveTerminal(parsed.cmd, spawn);
}
catch (e) {

View File

@@ -1,9 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2021",
"resolveJsonModule": true,
"module": "Node16",
"moduleResolution": "Node16",
"target": "esnext",
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": true
},

View File

@@ -1,114 +0,0 @@
<template>
<v-card raised>
<v-toolbar dark color="blue">
Console
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn @click="copy" v-on="on" text
><v-icon small> far fa-copy</v-icon>
</v-btn>
</template>
<span>Copy</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" text @click="expanded = !expanded">
<v-icon x-small>fa-angle-double-down</v-icon>
</v-btn>
</template>
<span>Toggle Expand</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn @click="clear" v-on="on" text
><v-icon small> fas fa-trash</v-icon>
</v-btn>
</template>
<span>Clear</span>
</v-tooltip>
</v-toolbar>
<div ref="terminal"></div>
</v-card>
</template>
<script>
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import eio from "engine.io-client";
import { sleep } from "../common/sleep";
import { getCurrentBaseUrl } from "../../../../../packages/client/src";
export default {
props: ["deviceId"],
socket: null,
buffer: [],
term: null,
watch: {
expanded(oldValue, newValue) {
if (this.expanded) this.term.resize(this.term.cols, this.term.rows * 2.5);
else this.term.resize(this.term.cols, this.term.rows / 2.5);
},
},
data() {
return {
expanded: false,
};
},
methods: {
async clear() {
this.term.clear();
this.buffer = [];
const plugins = await this.$scrypted.systemManager.getComponent(
"plugins"
);
plugins.clearConsole(this.deviceId);
},
reconnect(term) {
this.buffer = [];
const baseUrl = getCurrentBaseUrl();
const eioPath = `endpoint/@scrypted/core/engine.io/console/${this.deviceId}`;
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
const options = {
path: eioEndpoint,
};
const rootLocation = `${window.location.protocol}//${window.location.host}`;
this.socket = eio(rootLocation, options);
this.socket.on("message", (data) => {
this.buffer.push(Buffer.from(data));
term.write(new Uint8Array(data));
});
this.socket.on("close", async () => {
await sleep(1000);
this.reconnect(term);
});
},
copy() {
this.$copyText(Buffer.concat(this.buffer).toString());
},
},
mounted() {
const term = new Terminal({
theme: this.$vuetify.theme.dark ? undefined : {
foreground: 'black',
background: 'white',
cursor: 'black',
},
convertEol: true,
disableStdin: true,
scrollback: 10000,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(this.$refs.terminal);
fitAddon.fit();
this.term = term;
this.reconnect(term);
},
destroyed() {
this.socket?.close();
},
};
</script>

View File

@@ -22,11 +22,11 @@
</v-flex>
<v-flex xs12 v-if="showConsole" ref="consoleEl">
<ConsoleCard :deviceId="id"></ConsoleCard>
<PtyComponent :reconnect="true" :clearButton="true" @clear="clearConsole" :copyButton="true" title="Console" :hello="(device.nativeId || 'undefined') " nativeId="consoleservice" :control="false" :options="{ pluginId: device.pluginId }"></PtyComponent>
</v-flex>
<v-flex xs12 v-if="showRepl" ref="replEl">
<REPLCard :deviceId="id"></REPLCard>
<PtyComponent :copyButton="true" title="REPL" :hello="(device.nativeId || 'undefined')" nativeId="replservice" :control="false" :options="{ pluginId: device.pluginId }"></PtyComponent>
</v-flex>
<v-flex xs12 md7>
<v-layout row wrap>
@@ -198,8 +198,7 @@ import VueSlider from "vue-slider-component";
import "vue-slider-component/theme/material.css";
import LogCard from "./builtin/LogCard.vue";
import ConsoleCard from "./ConsoleCard.vue";
import REPLCard from "./REPLCard.vue";
import PtyComponent from "./builtin/PtyComponent.vue";
import {
getComponentWebPath,
getDeviceViewPath,
@@ -380,8 +379,7 @@ export default {
PluginAdvancedUpdate,
VueSlider,
LogCard,
ConsoleCard,
REPLCard,
PtyComponent,
Readme,
Storage,
@@ -474,6 +472,12 @@ export default {
onChange() {
// console.log(JSON.stringify(this.device));
},
async clearConsole() {
const plugins = await this.$scrypted.systemManager.getComponent(
"plugins"
);
plugins.clearConsole(this.device.id);
},
cleanupListener() {
if (this.listener) {
this.listener.removeListener();

View File

@@ -1,58 +0,0 @@
<template>
<v-card raised>
<v-toolbar dark color="blue"> JavaScript REPL </v-toolbar>
<div ref="terminal"></div>
</v-card>
</template>
<script>
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import eio from "engine.io-client";
import { getCurrentBaseUrl } from "../../../../../packages/client/src";
export default {
props: ["deviceId"],
socket: null,
mounted() {
const term = new Terminal({
theme: this.$vuetify.theme.dark
? undefined
: {
foreground: "black",
background: "white",
cursor: "black",
},
convertEol: true,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(this.$refs.terminal);
fitAddon.fit();
const baseUrl = getCurrentBaseUrl();
const eioPath = `endpoint/@scrypted/core/engine.io/repl/${this.deviceId}`;
const eioEndpoint = baseUrl ? new URL(eioPath, baseUrl).pathname : '/' + eioPath;
const options = {
path: eioEndpoint,
};
const rootLocation = `${window.location.protocol}//${window.location.host}`;
this.socket = eio(rootLocation, options);
this.socket.on("message", (data) => {
term.write(new Uint8Array(data));
});
term.onData((data) => {
this.socket.send(data);
});
term.onBinary((data) => {
this.socket.send(data);
});
},
destroyed() {
this.socket?.close();
},
};
</script>

View File

@@ -0,0 +1,187 @@
<template>
<v-card raised>
<v-toolbar dark color="blue">{{ title }}
<v-tooltip bottom v-if="copyButton">
<template v-slot:activator="{ on }">
<v-btn @click="copy" v-on="on" text
><v-icon small> far fa-copy</v-icon>
</v-btn>
</template>
<span>Copy</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn v-on="on" text @click="expanded = !expanded">
<v-icon x-small>fa-angle-double-down</v-icon>
</v-btn>
</template>
<span>Toggle Expand</span>
</v-tooltip>
<v-tooltip bottom v-if="clearButton">
<template v-slot:activator="{ on }">
<v-btn @click="clear" v-on="on" text
><v-icon small>fas fa-trash</v-icon>
</v-btn>
</template>
<span>Clear</span>
</v-tooltip>
</v-toolbar>
<div ref="terminal"></div>
</v-card>
</template>
<script>
import { createAsyncQueue } from "@scrypted/common/src/async-queue";
import { Deferred } from "@scrypted/common/src/deferred";
import { sleep } from "@scrypted/common/src/sleep";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
export default {
term: null,
buffer: [],
unmounted: null,
props: {
nativeId: String,
title: String,
// data sent to the pty service (repl/console) to route to correct device.
hello: String,
options: Object,
control: Boolean,
copyButton: Boolean,
clearButton: Boolean,
reconnect: Boolean,
},
destroyed() {
this.unmounted.resolve();
},
mounted() {
this.unmounted = new Deferred();
const term = new Terminal({
theme: this.$vuetify.theme.dark
? undefined
: {
foreground: "black",
background: "white",
cursor: "black",
},
convertEol: true,
});
this.term = term;
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(this.$refs.terminal);
fitAddon.fit();
this.connectPty(term);
},
watch: {
expanded(oldValue, newValue) {
if (this.expanded) this.term.resize(this.term.cols, this.term.rows * 2.5);
else this.term.resize(this.term.cols, this.term.rows / 2.5);
},
},
data() {
return {
expanded: false,
};
},
methods: {
async clear() {
this.term.clear();
this.buffer = [];
this.$emit("clear");
},
copy() {
this.$copyText(Buffer.concat(this.buffer).toString());
},
async connectPty(term) {
this.buffer = [];
const termSvcRaw = this.$scrypted.systemManager.getDeviceByName("@scrypted/core");
const termSvc = await termSvcRaw.getDevice(this.$props.nativeId);
const termSvcDirect = await this.$scrypted.connectRPCObject(termSvc);
const dataQueue = createAsyncQueue();
this.unmounted.promise.then(() => dataQueue.end());
if (this.$props.hello) {
const hello = Buffer.from(this.$props.hello, 'utf8');
dataQueue.enqueue(hello);
}
const ctrlQueue = createAsyncQueue();
if (!this.$props.control)
ctrlQueue.end();
ctrlQueue.enqueue({ interactive: true });
ctrlQueue.enqueue({ dim: { cols: term.cols, rows: term.rows } });
let bufferedLength = 0;
const MAX_BUFFERED_LENGTH = 64000;
async function dataQueueEnqueue(data) {
bufferedLength += data.length;
const promise = dataQueue.enqueue(data).then(() => bufferedLength -= data.length);
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
term.setOption("disableStdin", true);
await promise;
if (bufferedLength < MAX_BUFFERED_LENGTH)
term.setOption("disableStdin", false);
}
}
term.onData(data => dataQueueEnqueue(Buffer.from(data, 'utf8')));
term.onBinary(data => dataQueueEnqueue(Buffer.from(data, 'binary')));
term.onResize(dim => {
ctrlQueue.enqueue({ dim });
ctrlQueue.enqueue(Buffer.alloc(0));
});
async function* localGenerator() {
while (true) {
const ctrlBuffers = ctrlQueue.clear();
if (ctrlBuffers.length) {
for (const ctrl of ctrlBuffers) {
yield JSON.stringify(ctrl);
}
continue;
}
const dataBuffers = dataQueue.clear();
if (dataBuffers.length === 0) {
const buf = await dataQueue.dequeue();
if (buf.length)
yield buf;
continue;
}
const concat = Buffer.concat(dataBuffers);
if (concat.length)
yield concat;
}
}
const remoteGenerator = await termSvcDirect.connectStream(localGenerator(), this.$props.options);
try {
for await (const message of remoteGenerator) {
if (!message) {
break;
}
const buffer = Buffer.from(message);
if (this.$props.copyButton) {
this.buffer.push(buffer);
}
term.write(new Uint8Array(message));
}
}
finally {
if (!this.$props.reconnect)
return;
await sleep(1000);
if (this.unmounted.finished)
return;
this.connectPty(term);
}
}
},
};
</script>

View File

@@ -171,6 +171,8 @@ export default {
const serviceControl = await this.$scrypted.systemManager.getComponent(
"service-control"
);
// legacy command that exits npx scrypted.
await serviceControl.exit().catch(() => {});
await serviceControl.restart();
},
async doUpdateAndRestart() {

View File

@@ -1,96 +0,0 @@
<template>
<v-card raised>
<v-toolbar dark color="blue"> Terminal </v-toolbar>
<div ref="terminal" style="height: 700px"></div>
</v-card>
</template>
<script>
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { createAsyncQueue } from "@scrypted/common/src/async-queue";
export default {
mounted() {
const term = new Terminal({
theme: this.$vuetify.theme.dark
? undefined
: {
foreground: "black",
background: "white",
cursor: "black",
},
convertEol: true,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(this.$refs.terminal);
fitAddon.fit();
this.setupShell(term);
},
methods: {
async setupShell(term) {
const termSvcRaw = this.$scrypted.systemManager.getDeviceByName("@scrypted/core");
const termSvc = await termSvcRaw.getDevice("terminalservice");
const termSvcDirect = await this.$scrypted.connectRPCObject(termSvc);
const dataQueue = createAsyncQueue();
const ctrlQueue = createAsyncQueue();
ctrlQueue.enqueue({ interactive: true });
ctrlQueue.enqueue({ dim: { cols: term.cols, rows: term.rows } });
let bufferedLength = 0;
const MAX_BUFFERED_LENGTH = 64000;
async function dataQueueEnqueue(data) {
bufferedLength += data.length;
const promise = dataQueue.enqueue(data).then(() => bufferedLength -= data.length);
if (bufferedLength >= MAX_BUFFERED_LENGTH) {
term.setOption("disableStdin", true);
await promise;
if (bufferedLength < MAX_BUFFERED_LENGTH)
term.setOption("disableStdin", false);
}
}
term.onData(data => dataQueueEnqueue(Buffer.from(data, 'utf8')));
term.onBinary(data => dataQueueEnqueue(Buffer.from(data, 'binary')));
term.onResize(dim => {
ctrlQueue.enqueue({ dim });
ctrlQueue.enqueue(Buffer.alloc(0));
});
async function* localGenerator() {
while (true) {
const ctrlBuffers = ctrlQueue.clear();
if (ctrlBuffers.length) {
for (const ctrl of ctrlBuffers) {
yield JSON.stringify(ctrl);
}
continue;
}
const dataBuffers = dataQueue.clear();
if (dataBuffers.length === 0) {
const buf = await dataQueue.dequeue();
if (buf.length)
yield buf;
continue;
}
const concat = Buffer.concat(dataBuffers);
if (concat.length)
yield concat;
}
}
const remoteGenerator = await termSvcDirect.connectStream(localGenerator());
for await (const message of remoteGenerator) {
if (!message) {
break;
}
term.write(new Uint8Array(Buffer.from(message)));
}
}
},
};
</script>

View File

@@ -57,7 +57,10 @@ export default {
t += `<tspan x='${x}' dy='${toffset}em'>${Math.round(detection.score * 100) / 100}</tspan>`
toffset -= 1.2;
}
const tname = detection.className + (detection.id ? `: ${detection.id}` : '')
const tname = detection.className
+ (detection.id || detection.label ? ':' : '')
+ (detection.id ? ` ${detection.id}` : '')
+ (detection.label ? ` ${detection.label}` : '')
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
const fs = 30 * svgScale;

View File

@@ -38,8 +38,6 @@
<script>
import RPCInterface from "../RPCInterface.vue";
import types from "!!raw-loader!@scrypted/types/dist/index.d.ts";
import sdk from "!!raw-loader!@scrypted/sdk/dist/src/index.d.ts";
import * as monaco from "monaco-editor";
function monacoEvalDefaults() {
@@ -63,33 +61,6 @@ function monacoEvalDefaults() {
}
)
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
`${types}
${sdk}
declare global {
${types.replace("export interface", "interface")}
const log: Logger;
const deviceManager: DeviceManager;
const endpointManager: EndpointManager;
const mediaManager: MediaManager;
const systemManager: SystemManager;
const eventSource: any;
const eventDetails: EventDetails;
const eventData: any;
}
`,
"node_modules/@types/scrypted__sdk/types.d.ts"
);
monaco.languages.typescript.typescriptDefaults.addExtraLib(
sdk,
"node_modules/@types/scrypted__sdk/index.d.ts"
);
}
export default {

View File

@@ -9,7 +9,7 @@ import PluginComponent from "./components/plugin/PluginComponent.vue";
import InstallPlugin from "./components/plugin/InstallPlugin.vue";
import LogComponent from "./components/builtin/LogComponent.vue";
import SettingsComponent from "./components/builtin/SettingsComponent.vue";
import ShellComponent from "./components/builtin/ShellComponent.vue";
import PtyComponent from "./components/builtin/PtyComponent.vue";
import UsersComponent from "./components/UsersComponent.vue";
let router = new VueRouter({
@@ -28,7 +28,12 @@ let router = new VueRouter({
},
{
path: "/component/shell",
component: ShellComponent,
component: PtyComponent,
props: {
title: "Terminal",
nativeId: "terminalservice",
control: true,
}
},
{
path: "/component/plugin",

View File

@@ -103,8 +103,8 @@ module.exports = {
devServer: {
disableHostCheck: true,
host: '127.0.0.1',
https: true,
port: 8081,
// https: true,
port: 8082,
progress: false,
proxy: {
'^/(login|logout|static|endpoint|whitelist|web|engine.io)': proxyOpts,

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/coreml",
"version": "0.1.28",
"version": "0.1.45",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.1.28",
"version": "0.1.45",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}

View File

@@ -34,6 +34,7 @@
"type": "API",
"interfaces": [
"Settings",
"DeviceProvider",
"ObjectDetection",
"ObjectDetectionPreview"
]
@@ -41,5 +42,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.1.28"
"version": "0.1.45"
}

1
plugins/coreml/src/common Symbolic link
View File

@@ -0,0 +1 @@
../../openvino/src/common

View File

@@ -1,24 +1,42 @@
from __future__ import annotations
import ast
import asyncio
import concurrent.futures
import os
import re
from typing import Any, Tuple
from typing import Any, List, Tuple
import coremltools as ct
import scrypted_sdk
from PIL import Image
from scrypted_sdk import Setting, SettingValue
import yolo
from predict import Prediction, PredictPlugin, Rectangle
from common import yolo
from coreml.recognition import CoreMLRecognition
from predict import Prediction, PredictPlugin
from predict.rectangle import Rectangle
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "CoreML-Predict")
predictExecutor = concurrent.futures.ThreadPoolExecutor(1, "CoreML-Predict")
availableModels = [
"Default",
"scrypted_yolov9c_320",
"scrypted_yolov9c",
"scrypted_yolov6n_320",
"scrypted_yolov6n",
"scrypted_yolov6s_320",
"scrypted_yolov6s",
"scrypted_yolov8n_320",
"scrypted_yolov8n",
"ssdlite_mobilenet_v2",
"yolov4-tiny",
]
def parse_label_contents(contents: str):
lines = contents.splitlines()
lines = contents.split(",")
lines = [line for line in lines if line.strip()]
ret = {}
for row_number, content in enumerate(lines):
pair = re.split(r"[:\s]+", content.strip(), maxsplit=1)
@@ -29,41 +47,64 @@ def parse_label_contents(contents: str):
return ret
class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Settings):
def parse_labels(userDefined):
yolo = userDefined.get("names") or userDefined.get("yolo.names")
if yolo:
j = ast.literal_eval(yolo)
ret = {}
for k, v in j.items():
ret[int(k)] = v
return ret
classes = userDefined.get("classes")
if not classes:
raise Exception("no classes found in model metadata")
return parse_label_contents(classes)
class CoreMLPlugin(PredictPlugin, scrypted_sdk.Settings, scrypted_sdk.DeviceProvider):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
model = self.storage.getItem("model") or "Default"
if model == "Default":
model = "yolov8n_320"
if model == "Default" or model not in availableModels:
if model != "Default":
self.storage.setItem("model", "Default")
model = "scrypted_yolov9c_320"
self.yolo = "yolo" in model
self.yolov8 = "yolov8" in model
model_version = "v2"
self.scrypted_yolo = "scrypted_yolo" in model
self.scrypted_model = "scrypted" in model
model_version = "v7"
mlmodel = "model" if self.scrypted_yolo else model
print(f"model: {model}")
if not self.yolo:
# todo convert these to mlpackage
labelsFile = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{model}/coco_labels.txt",
"coco_labels.txt",
)
modelFile = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{model}/{model}.mlmodel",
f"https://github.com/koush/coreml-models/raw/main/{model}/{mlmodel}.mlmodel",
f"{model}.mlmodel",
)
else:
if self.yolov8:
modelFile = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{model}/{model}.mlmodel",
f"{model}.mlmodel",
)
if self.scrypted_yolo:
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{f}",
f"{model_version}/{f}",
)
modelFile = os.path.dirname(p)
else:
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/FeatureDescriptions.json",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/Metadata.json",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{model}.mlmodel",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
@@ -74,25 +115,42 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
)
modelFile = os.path.dirname(p)
labelsFile = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{model}/coco_80cl.txt",
f"{model_version}/{model}/coco_80cl.txt",
)
self.model = ct.models.MLModel(modelFile)
self.modelspec = self.model.get_spec()
self.inputdesc = self.modelspec.description.input[0]
self.inputheight = self.inputdesc.type.imageType.height
self.inputwidth = self.inputdesc.type.imageType.width
self.input_name = self.model.get_spec().description.input[0].name
labels_contents = open(labelsFile, "r").read()
self.labels = parse_label_contents(labels_contents)
# csv in mobilenet model
# self.modelspec.description.metadata.userDefined['classes']
self.labels = parse_labels(self.modelspec.description.metadata.userDefined)
self.loop = asyncio.get_event_loop()
self.minThreshold = 0.2
asyncio.ensure_future(self.prepareRecognitionModels(), loop=self.loop)
async def prepareRecognitionModels(self):
try:
await scrypted_sdk.deviceManager.onDevicesChanged(
{
"devices": [
{
"nativeId": "recognition",
"type": scrypted_sdk.ScryptedDeviceType.Builtin.value,
"interfaces": [
scrypted_sdk.ScryptedInterface.ObjectDetection.value,
],
"name": "CoreML Recognition",
}
]
}
)
except:
pass
async def getDevice(self, nativeId: str) -> Any:
return CoreMLRecognition(nativeId)
async def getSettings(self) -> list[Setting]:
model = self.storage.getItem("model") or "Default"
return [
@@ -100,13 +158,7 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
"key": "model",
"title": "Model",
"description": "The detection model used to find objects.",
"choices": [
"Default",
"ssdlite_mobilenet_v2",
"yolov4-tiny",
"yolov8n",
"yolov8n_320",
],
"choices": availableModels,
"value": model,
},
]
@@ -122,25 +174,23 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
def get_input_size(self) -> Tuple[float, float]:
return (self.inputwidth, self.inputheight)
async def detect_batch(self, inputs: List[Any]) -> List[Any]:
out_dicts = await asyncio.get_event_loop().run_in_executor(
predictExecutor, lambda: self.model.predict(inputs)
)
return out_dicts
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
objs = []
# run in executor if this is the plugin loop
if self.yolo:
input_name = "image" if self.yolov8 else "input_1"
if asyncio.get_event_loop() is self.loop:
out_dict = await asyncio.get_event_loop().run_in_executor(
predictExecutor, lambda: self.model.predict({input_name: input})
)
else:
out_dict = self.model.predict({input_name: input})
out_dict = await self.queue_batch({self.input_name: input})
if self.yolov8:
out_blob = out_dict["var_914"]
var_914 = out_dict["var_914"]
results = var_914[0]
objs = yolo.parse_yolov8(results)
if self.scrypted_yolo:
results = list(out_dict.values())[0][0]
objs = yolo.parse_yolov9(results)
ret = self.create_detection_result(objs, src_size, cvss)
return ret
@@ -174,17 +224,12 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
ret = self.create_detection_result(objs, src_size, cvss)
return ret
if asyncio.get_event_loop() is self.loop:
out_dict = await asyncio.get_event_loop().run_in_executor(
predictExecutor,
lambda: self.model.predict(
{"image": input, "confidenceThreshold": self.minThreshold}
),
)
else:
out_dict = self.model.predict(
out_dict = await asyncio.get_event_loop().run_in_executor(
predictExecutor,
lambda: self.model.predict(
{"image": input, "confidenceThreshold": self.minThreshold}
)
),
)
coordinatesList = out_dict["coordinates"].astype(float)

View File

@@ -0,0 +1,132 @@
from __future__ import annotations
import concurrent.futures
import os
import coremltools as ct
import numpy as np
# import Quartz
# from Foundation import NSData, NSMakeSize
# import Vision
from predict.recognize import RecognizeDetection
def euclidean_distance(arr1, arr2):
return np.linalg.norm(arr1 - arr2)
def cosine_similarity(vector_a, vector_b):
dot_product = np.dot(vector_a, vector_b)
norm_a = np.linalg.norm(vector_a)
norm_b = np.linalg.norm(vector_b)
similarity = dot_product / (norm_a * norm_b)
return similarity
predictExecutor = concurrent.futures.ThreadPoolExecutor(8, "Vision-Predict")
class CoreMLRecognition(RecognizeDetection):
def __init__(self, nativeId: str | None = None):
super().__init__(nativeId=nativeId)
def downloadModel(self, model: str):
model_version = "v7"
mlmodel = "model"
files = [
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
f"{model}/{model}.mlpackage/Data/com.apple.CoreML/{mlmodel}.mlmodel",
f"{model}/{model}.mlpackage/Manifest.json",
]
for f in files:
p = self.downloadFile(
f"https://github.com/koush/coreml-models/raw/main/{f}",
f"{model_version}/{f}",
)
modelFile = os.path.dirname(p)
model = ct.models.MLModel(modelFile)
inputName = model.get_spec().description.input[0].name
return model, inputName
def predictDetectModel(self, input):
model, inputName = self.detectModel
out_dict = model.predict({inputName: input})
results = list(out_dict.values())[0][0]
return results
def predictFaceModel(self, input):
model, inputName = self.faceModel
out_dict = model.predict({inputName: input})
return out_dict["var_2167"][0]
def predictTextModel(self, input):
model, inputName = self.textModel
out_dict = model.predict({inputName: input})
preds = out_dict["linear_2"]
return preds
# def predictVision(self, input: Image.Image) -> asyncio.Future[list[Prediction]]:
# buffer = input.tobytes()
# myData = NSData.alloc().initWithBytes_length_(buffer, len(buffer))
# input_image = (
# Quartz.CIImage.imageWithBitmapData_bytesPerRow_size_format_options_(
# myData,
# 4 * input.width,
# NSMakeSize(input.width, input.height),
# Quartz.kCIFormatRGBA8,
# None,
# )
# )
# request_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
# input_image, None
# )
# loop = self.loop
# future = loop.create_future()
# def detect_face_handler(request, error):
# observations = request.results()
# if error:
# loop.call_soon_threadsafe(future.set_exception, Exception())
# else:
# objs = []
# for o in observations:
# confidence = o.confidence()
# bb = o.boundingBox()
# origin = bb.origin
# size = bb.size
# l = origin.x * input.width
# t = (1 - origin.y - size.height) * input.height
# w = size.width * input.width
# h = size.height * input.height
# prediction = Prediction(
# 0, confidence, from_bounding_box((l, t, w, h))
# )
# objs.append(prediction)
# loop.call_soon_threadsafe(future.set_result, objs)
# request = (
# Vision.VNDetectFaceRectanglesRequest.alloc().initWithCompletionHandler_(
# detect_face_handler
# )
# )
# error = request_handler.performRequests_error_([request], None)
# return future
# async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
# future = await asyncio.get_event_loop().run_in_executor(
# predictExecutor,
# lambda: self.predictVision(input),
# )
# objs = await future
# ret = self.create_detection_result(objs, src_size, cvss)
# return ret

View File

@@ -1,6 +1,2 @@
#
coremltools==7.0b2
# pillow for anything not intel linux, pillow-simd is available on x64 linux
Pillow>=5.4.1; sys_platform != 'linux' or platform_machine != 'x86_64'
pillow-simd; sys_platform == 'linux' and platform_machine == 'x86_64'
coremltools==7.1
Pillow>=5.4.1

Some files were not shown because too many files have changed in this diff Show More