Compare commits

...

263 Commits

Author SHA1 Message Date
Koushik Dutta
4121cbd400 prerelease 2023-03-18 08:05:24 -07:00
Koushik Dutta
2d3bb8798d server: disable python stdout buffering 2023-03-18 08:05:14 -07:00
Koushik Dutta
b7b6ac0f87 python-codecs: fix python3.7 maybe 2023-03-17 23:54:32 -07:00
Koushik Dutta
e5fb65d75e prerelease 2023-03-17 23:42:47 -07:00
Koushik Dutta
290b73f3d9 python-codecs: fix hw acceleration 2023-03-17 23:42:33 -07:00
Koushik Dutta
f717e87306 snapshot: include ffmpeg path 2023-03-17 23:37:20 -07:00
Koushik Dutta
b80ac7c60d prebeta 2023-03-17 23:21:33 -07:00
Koushik Dutta
997a4732ec server: additional python rpc transport fixes 2023-03-17 23:21:07 -07:00
Koushik Dutta
6e08f11578 snapshot/python-codecs: move high performance native image library to larger package as optional dependency 2023-03-17 22:58:29 -07:00
Koushik Dutta
87c4814e6f prebeta 2023-03-17 22:22:39 -07:00
Koushik Dutta
2e0e009719 server: update publish scripts 2023-03-17 22:22:30 -07:00
Koushik Dutta
77399038e9 server: clean up python rpc transports 2023-03-17 22:21:07 -07:00
Koushik Dutta
fae66619fb prepublish 2023-03-17 19:16:50 -07:00
Koushik Dutta
d979b9ec0c server: connection.poll should provide None to block forever 2023-03-17 19:16:15 -07:00
Koushik Dutta
975319a65d motion: implement a default inclusion zone that prevents on screen clocks from triggering motion 2023-03-17 16:19:50 -07:00
Koushik Dutta
7b5aa4ba2d python-codecs: remove erroneous libav from gstreamer settings 2023-03-17 16:19:20 -07:00
Koushik Dutta
670739c82b python-codecs: restructure, add gstreamer decoder option 2023-03-17 10:28:41 -07:00
Koushik Dutta
8511bd15a8 server: update package lock 2023-03-16 23:59:19 -07:00
Koushik Dutta
06d3c89274 prepublish 2023-03-16 23:59:10 -07:00
Koushik Dutta
e13f3eb2f1 server: add python forked processes to stats 2023-03-16 23:59:01 -07:00
Koushik Dutta
001918d613 predict: fix detections from webui 2023-03-16 23:58:45 -07:00
Koushik Dutta
c859c3aa40 detect: publish plugins with new video pipeline support 2023-03-16 23:40:33 -07:00
Koushik Dutta
2bce019677 predict: make models a separate download 2023-03-16 23:29:02 -07:00
Koushik Dutta
6ba3386157 detect: fix peer kill causing exception inside finally handler 2023-03-16 22:10:25 -07:00
Koushik Dutta
51e66d98f9 videoanalysis: changing motion detect mode should restart motion detection 2023-03-16 22:09:56 -07:00
Koushik Dutta
6484804649 server: update package lock 2023-03-16 20:37:54 -07:00
Koushik Dutta
b2a05c099d prepublish 2023-03-16 20:37:42 -07:00
Koushik Dutta
898331da4c Merge branch 'main' of github.com:koush/scrypted 2023-03-16 20:37:33 -07:00
Koushik Dutta
9044e782b2 python-codecs: add gray decoding support 2023-03-16 20:37:28 -07:00
Koushik Dutta
aedb985941 detect: support motion on new pipeline 2023-03-16 20:37:12 -07:00
Koushik Dutta
9ba22e4058 server: fix python rpc kill handling 2023-03-16 20:33:09 -07:00
Alex Leeds
ab0afb61ae ring: add video clips support (#635)
* ring: add video clips support

* fix merge
2023-03-16 18:40:36 -07:00
Alex Leeds
bf00ba0adc ring: add support for locks (#634) 2023-03-16 18:32:20 -07:00
Koushik Dutta
d564cf1b62 server: update package lock 2023-03-16 11:13:24 -07:00
Koushik Dutta
544dfb3b24 Update rtsp-proxy.ts 2023-03-16 10:40:19 -07:00
Koushik Dutta
cf9af910be rtsp: rtsp proxy example 2023-03-16 10:03:24 -07:00
Koushik Dutta
e2e65f93af prepublish 2023-03-16 09:37:34 -07:00
Koushik Dutta
b271567428 server: Fix device initialization on first report 2023-03-16 09:37:25 -07:00
Koushik Dutta
a88a295d9a server: fixup project file 2023-03-15 23:09:16 -07:00
Koushik Dutta
38ba31ca7d tensorflow-lite: use multiple tpu 2023-03-15 23:08:48 -07:00
Koushik Dutta
1c8ff2493b coreml: move prediction onto background thread 2023-03-15 23:04:45 -07:00
Koushik Dutta
5c9f62e6b6 videoanalysis: add snapshot pipeline 2023-03-15 23:04:13 -07:00
Koushik Dutta
6fd8018c52 python-codecs: fix nre 2023-03-15 23:02:50 -07:00
Koushik Dutta
d900ddf5f1 mac: fix erroneous typing installation 2023-03-15 21:54:17 -07:00
Koushik Dutta
e3a8d311ce python-codecs: add libav support 2023-03-15 20:33:44 -07:00
Koushik Dutta
8bbc3d5470 videoanalysis: generator cleanup 2023-03-15 17:18:28 -07:00
Koushik Dutta
00cf987cec videoanalysis: reimplemnet snapshots for new pipeline 2023-03-15 17:03:34 -07:00
Koushik Dutta
7e5dcae64a webrtc/alexa: add option to disable TURN on peers that already have externally reachable addresses 2023-03-15 10:31:25 -07:00
Koushik Dutta
cb67237d7c server: update package lock 2023-03-15 01:28:39 -07:00
Koushik Dutta
4be848c440 prepublish 2023-03-15 01:28:05 -07:00
Koushik Dutta
b33422b066 server: fix python fork hangs 2023-03-15 01:28:01 -07:00
Koushik Dutta
77418684da server: publish 2023-03-14 23:50:22 -07:00
Koushik Dutta
08cf9f7774 prepublish 2023-03-14 23:49:51 -07:00
Koushik Dutta
9f2fabf9c0 Merge branch 'main' of github.com:koush/scrypted 2023-03-14 23:47:24 -07:00
Koushik Dutta
e2e1c7be44 server: remove python log statement 2023-03-14 23:47:05 -07:00
Koushik Dutta
ba030ba197 server: fix multiprocessing blocking read on linux 2023-03-14 23:45:06 -07:00
Koushik Dutta
a4f37bdc16 snapshot: publish 2023-03-14 23:42:33 -07:00
Koushik Dutta
f6c7b00562 tensorflow-lite: fix numpy serialization issue 2023-03-14 23:41:55 -07:00
Koushik Dutta
b951614f7c Merge branch 'main' of github.com:koush/scrypted 2023-03-14 20:13:28 -07:00
Koushik Dutta
f1dfdb3494 coreml: revert tracker dependency removal 2023-03-14 20:13:22 -07:00
Nick Berardi
ffbd25b13b alexa: set screen ratio to 720p (#625) 2023-03-14 18:40:47 -07:00
Koushik Dutta
4f03fe2420 docker: fix pyvips cffi mismatch 2023-03-14 18:00:10 -07:00
Koushik Dutta
ffdb386afa mac: include libvips in installer 2023-03-14 17:25:47 -07:00
Koushik Dutta
9eeeaa79d0 docker: include libvips 2023-03-14 16:08:22 -07:00
Koushik Dutta
4163142d1e Merge branch 'main' of github.com:koush/scrypted 2023-03-14 15:45:35 -07:00
Koushik Dutta
71cddc67e0 predict: publish new pipeline support 2023-03-14 15:45:30 -07:00
Alex Leeds
2cbc4eb54f eufy: support multiple p2p streams (#624) 2023-03-14 15:26:46 -07:00
Koushik Dutta
fc94fb4221 core: republish 2023-03-14 15:21:13 -07:00
Koushik Dutta
85ed41c590 server: publish 2023-03-14 15:15:09 -07:00
Koushik Dutta
59f889a200 prepublish 2023-03-14 15:15:05 -07:00
Koushik Dutta
7dc476fe02 prepublish 2023-03-14 15:14:59 -07:00
Koushik Dutta
f5070f1ff1 server: publish 2023-03-14 15:14:41 -07:00
Koushik Dutta
15283e13f0 prepublish 2023-03-14 15:14:23 -07:00
Koushik Dutta
0cde5bf8e7 prepublish 2023-03-14 15:14:10 -07:00
Koushik Dutta
fe3a1a023d prepublish 2023-03-14 15:14:02 -07:00
Koushik Dutta
369dcff2bd server: support large file transfers on engine io 2023-03-14 14:50:47 -07:00
Koushik Dutta
ed341a12b1 predict: rgba to rgb conversion 2023-03-14 14:50:28 -07:00
Koushik Dutta
00e523e268 core: add object detection ui 2023-03-14 14:50:04 -07:00
Koushik Dutta
4e25aedbe7 python-codecs: multiprocessing decode 2023-03-14 10:22:01 -07:00
Koushik Dutta
45bd3cbb7c server: fix various python mutiprocesisng quirks 2023-03-14 10:21:45 -07:00
Koushik Dutta
8e34bc2130 server: fix dangling thread if glib main loop fails 2023-03-14 09:18:00 -07:00
Koushik Dutta
457fc96332 predict: support for new pipeline redetection 2023-03-14 09:16:54 -07:00
Koushik Dutta
e2186401bf videoanalysis: new working pipeline 2023-03-14 09:16:34 -07:00
Koushik Dutta
a19d916ef0 python-codecs: improve memory management 2023-03-14 09:16:08 -07:00
Koushik Dutta
42bc7dc644 rebroadcast: publish update, current version was using actual addresses? 2023-03-14 08:52:38 -07:00
Koushik Dutta
f9d6308154 rpc: python rpc should be killed on disconnect 2023-03-13 17:10:06 -07:00
Koushik Dutta
dcb6627fb1 predict: publish fix that validates settings input 2023-03-13 11:15:45 -07:00
Koushik Dutta
1d5c71d617 videoanalysis: publish 2023-03-13 10:28:07 -07:00
Koushik Dutta
d5157fb868 predict: new detection pipeline around 50% faster! 2023-03-13 10:17:38 -07:00
Koushik Dutta
98096845dc rtp: add utility method for adding timestamps 2023-03-13 10:17:09 -07:00
Koushik Dutta
28ac97f4c9 predict: new pipeline 2023-03-12 22:11:06 -07:00
Koushik Dutta
2fc39e3979 videoanalysis: new pipeline 2023-03-12 22:10:40 -07:00
Koushik Dutta
9c89c3c2b8 snapshot: vips fixes 2023-03-12 22:10:15 -07:00
Koushik Dutta
15c7747f48 sdk: update 2023-03-12 22:09:59 -07:00
Koushik Dutta
940d4b7fd4 rpc: various python fixes 2023-03-12 22:09:50 -07:00
Koushik Dutta
a1c8ce754e python-codecs: working prototype 2023-03-12 22:09:33 -07:00
Koushik Dutta
5e6364850a onvif: fix ptz causing creation issues 2023-03-12 10:44:21 -07:00
Koushik Dutta
8df52e7595 python-codecs: wip 2023-03-11 20:01:26 -08:00
Koushik Dutta
1e004d6700 rpc: fixup various async iterator bugs, add memoryview support to python 2023-03-11 19:38:43 -08:00
Koushik Dutta
4570f9cd38 python-codecs: wip 2023-03-11 00:17:50 -08:00
Koushik Dutta
601cd39ba4 rpc: fix proxied iterator proxy 2023-03-10 21:38:48 -08:00
Koushik Dutta
923475fab2 Merge branch 'main' of github.com:koush/scrypted 2023-03-10 19:46:59 -08:00
Koushik Dutta
21ce5dfad4 sdk: image support 2023-03-10 19:46:51 -08:00
Koushik Dutta
2bd3592aad server: fix mediaobject polymorphism 2023-03-10 19:46:38 -08:00
Koushik Dutta
44f083ca23 webrtc: remove potential converter with permission escalation 2023-03-10 19:46:03 -08:00
Koushik Dutta
cc7271f0a2 snapshot: use libvips 2023-03-10 19:45:37 -08:00
Koushik Dutta
11a1a1134d predict: validate args 2023-03-10 16:50:11 -08:00
Alex Leeds
70cfa13e67 eufy: motion, partial livestream removal & minor improvement in snapshots (#618) 2023-03-10 16:16:36 -08:00
Koushik Dutta
291f90b2b2 rtp: expose child process in rtp forwarder 2023-03-10 11:55:22 -08:00
Koushik Dutta
d0ae7eb841 eufy: all cleaned up 2023-03-10 11:11:13 -08:00
Koushik Dutta
8444102cca eufy: functional audio 2023-03-10 10:49:45 -08:00
Alex Leeds
5a1c052c77 eufy: support captcha (#616) 2023-03-10 09:46:42 -08:00
Koushik Dutta
fb7eeece54 eufy: more logging 2023-03-10 07:53:34 -08:00
Koushik Dutta
d479bcece9 eufy: fix encoder codecs 2023-03-10 07:34:04 -08:00
Koushik Dutta
deefac2347 eufy: fix encoder codecs 2023-03-10 07:33:04 -08:00
Koushik Dutta
53808a04b7 google-cloud-tts: move to org 2023-03-09 21:40:06 -08:00
Koushik Dutta
a1785c2658 tensorflow-legacy: remove 2023-03-09 21:37:56 -08:00
Koushik Dutta
601cf46b1e thermostat: move to org 2023-03-09 21:37:28 -08:00
Koushik Dutta
6bba1b1cbd eufy: fix output url 2023-03-09 20:56:22 -08:00
Koushik Dutta
ab0122420b eufy: codec copy 2023-03-09 20:55:48 -08:00
Koushik Dutta
74ae2aab91 eufy: try mpegts 2023-03-09 20:54:43 -08:00
Koushik Dutta
c5fa131a44 eufy: revert stream manager change 2023-03-09 20:44:00 -08:00
Koushik Dutta
8dcf4dda9f eufy: use ffmpeg and adts audio 2023-03-09 20:29:00 -08:00
Koushik Dutta
cd59125ada eufy: revert 2023-03-09 20:24:08 -08:00
Koushik Dutta
d284eb6738 eufy: mute audio 2023-03-09 19:07:42 -08:00
Koushik Dutta
a78cc943cc eufy: mark stream as scrypted parser safe 2023-03-09 19:07:22 -08:00
Koushik Dutta
7ddeda1595 eufy: add audio toggle 2023-03-09 19:06:20 -08:00
Koushik Dutta
f02dfa5e14 eufy: remove some logging 2023-03-09 18:56:31 -08:00
Koushik Dutta
b2a4f20381 eufy: audio maybe 2023-03-09 18:53:36 -08:00
Koushik Dutta
dec3c354f0 eufy: use per session live stream manager 2023-03-09 18:47:45 -08:00
Koushik Dutta
2ee581d48d Merge branch 'main' of github.com:koush/scrypted 2023-03-09 18:07:34 -08:00
Koushik Dutta
d74c3a3fc5 eufy: generate some timestamps 2023-03-09 18:07:29 -08:00
Nick Berardi
405d9f0c09 onvif: add absolute and speed support to movement (#612) 2023-03-09 17:17:08 -08:00
Koushik Dutta
db25c5babe Merge branch 'main' of github.com:koush/scrypted 2023-03-09 14:13:17 -08:00
Alex Leeds
d5c90ab8da eufy: add plugin (#614) 2023-03-09 14:13:03 -08:00
Koushik Dutta
81a5c143d6 snapshot: add/use sharp (libvips) 2023-03-09 09:29:28 -08:00
Koushik Dutta
ebf2176618 remote: wip 2023-03-08 13:36:55 -08:00
Koushik Dutta
f435f8eff5 sdk: update 2023-03-08 13:36:40 -08:00
Koushik Dutta
f8c16edaae Merge branch 'main' of github.com:koush/scrypted 2023-03-08 07:37:03 -08:00
Koushik Dutta
ea86065d99 tapo: add cloud password instructions 2023-03-08 07:36:57 -08:00
Alex Leeds
ed5c7b126c ring: update dependencies (#607) 2023-03-07 20:49:28 -08:00
Koushik Dutta
806e015823 tapo: make it searchable in plugin install 2023-03-07 16:55:15 -08:00
Koushik Dutta
41c4cbc96c client: update 2023-03-07 16:24:31 -08:00
Koushik Dutta
143a0b2c41 webrtc: startRtpForwarderProcess remove werift dependency 2023-03-07 16:24:22 -08:00
Koushik Dutta
f582db3f11 common: http message parsing helpers 2023-03-07 16:24:00 -08:00
Koushik Dutta
103855ca50 Merge branch 'main' of github.com:koush/scrypted 2023-03-07 16:07:22 -08:00
Koushik Dutta
70c6fe4c68 tapo: initial commit of two way audio 2023-03-07 16:07:15 -08:00
Nick Berardi
c85d45050f alexa: refactor code structure (#606) 2023-03-07 12:04:52 -08:00
Alex Leeds
16a39ac76a ring: update ring api client (#605) 2023-03-07 07:51:25 -08:00
Koushik Dutta
fdc7519db0 onvif: ptz 2023-03-06 18:17:54 -08:00
Koushik Dutta
83af0c5ec7 core: cleanup device discovery 2023-03-06 17:03:21 -08:00
Koushik Dutta
ee22686bff videoanalysis: prevent double motion detector or double object detector 2023-03-06 10:32:35 -08:00
Koushik Dutta
7dc1f9736a pam-diff: add support for motion objects 2023-03-06 10:10:16 -08:00
Koushik Dutta
6e2aa37d75 server: implement missing setMixins 2023-03-06 09:34:29 -08:00
Koushik Dutta
fbaa8a31cf predict: fix bug where memory can leak if detection fails
tf: request restart if detection fails
2023-03-06 09:34:04 -08:00
Koushik Dutta
fa89a5ad24 sort: fix crash if no detection id is provided 2023-03-06 09:33:38 -08:00
Koushik Dutta
464deaf35e cameras: fix bug where device creation fails when no name is provided 2023-03-06 09:33:15 -08:00
Koushik Dutta
9cc8f50ff7 client: update sdk 2023-03-06 09:32:53 -08:00
Koushik Dutta
c17a1184cc core: fix settings subgroup regression 2023-03-06 07:39:17 -08:00
Koushik Dutta
b5004739c3 core: fix wonky settings 2023-03-05 23:09:31 -08:00
Koushik Dutta
d01c0fa72b sdk: fix StorageSettings 'device' defaults 2023-03-05 22:39:04 -08:00
Koushik Dutta
bb9f3d5aab predict: revert object tracker changes until custom NVR detector with face recognition is in place 2023-03-05 22:38:36 -08:00
Koushik Dutta
b23daa6735 Merge branch 'main' of github.com:koush/scrypted 2023-03-05 21:36:54 -08:00
Koushik Dutta
bb8b0125b6 server/sdk: update 2023-03-05 21:36:50 -08:00
Koushik Dutta
8e5f44f998 server: add support for polymorphic media objects 2023-03-05 21:33:44 -08:00
Brett Jia
9015af4902 arlo: optimize event handling (#601)
* optimize event waiting by keying on properties

* bump 0.6.6

* interrupt cleanup for other tasks

* bump 0.6.7 for race condition fix
2023-03-05 19:25:47 -08:00
Koushik Dutta
7902a091a9 core: fix listener leak 2023-03-04 20:48:24 -08:00
Koushik Dutta
615357befb werift: update 2023-03-04 19:20:31 -08:00
Koushik Dutta
34b26c81dc server: fix bug where express sets Cache-Control: max-age=0 on all file responses 2023-03-04 19:19:52 -08:00
Koushik Dutta
ea99a54e1b cloud: cleanup logging 2023-03-04 19:18:36 -08:00
Koushik Dutta
f726826391 core: fix changing password escalating user privileges 2023-03-04 19:00:49 -08:00
Koushik Dutta
dc5148c856 rpc: dont throw on oneway methods even if the peer is closed 2023-03-04 18:59:55 -08:00
Koushik Dutta
373c11ffee webrtc: add connection logging 2023-03-04 18:34:45 -08:00
Koushik Dutta
bea1f019b4 server: update deps 2023-03-04 14:05:08 -08:00
Koushik Dutta
29c98777e9 server: add python plugin id to command line 2023-03-04 14:05:04 -08:00
Koushik Dutta
9eb5029128 cloud: Fix x-scrypted-cloud header to come from upstream proxy 2023-03-04 08:59:23 -08:00
Koushik Dutta
33607796d1 cloud: log incoming connections 2023-03-04 07:51:08 -08:00
Koushik Dutta
f23fa0c335 coreml: update deps 2023-03-03 23:39:43 -08:00
Koushik Dutta
e6cfecfc1a videoanalysis: configurable object tracker 2023-03-03 23:39:18 -08:00
Koushik Dutta
44346d5b33 server: fix python rpc connect 2023-03-03 23:34:15 -08:00
Koushik Dutta
19da68884b server: implement python connectRPCObject 2023-03-03 23:17:43 -08:00
Koushik Dutta
544349de8d snapshot: update sdk 2023-03-03 16:48:37 -08:00
Koushik Dutta
6f90b1a0e3 server: add support for direct ipc 2023-03-03 16:48:29 -08:00
Koushik Dutta
fbbb9163d7 sdk: add ipcObject 2023-03-03 14:56:40 -08:00
Koushik Dutta
445581eefa server: plugin worker cleanups 2023-03-03 11:36:15 -08:00
Koushik Dutta
096c036ea2 rpc: implement python async iterator 2023-03-02 21:03:29 -08:00
Koushik Dutta
b2e5801426 rpc: improve error serialization and handling 2023-03-02 16:02:48 -08:00
Koushik Dutta
41061854f1 rpc: add intrinsic support for async iterators 2023-03-02 13:49:20 -08:00
Koushik Dutta
d91e625973 sort-tracker: publish 2023-03-02 09:09:53 -08:00
Koushik Dutta
ec5b59a00c Merge branch 'main' of github.com:koush/scrypted 2023-03-01 21:34:12 -08:00
Koushik Dutta
172790b18f sdk: fix device StorageSetting deserialzation
predict: externalize tracker
2023-03-01 21:33:43 -08:00
Nick Berardi
de0e6ee955 unifi-protect: added new smart event and updated snapshot to use login (#595) 2023-03-01 20:13:01 -08:00
Koushik Dutta
69d7ff2ced Merge branch 'main' of github.com:koush/scrypted 2023-03-01 14:51:55 -08:00
Koushik Dutta
3c237eac91 tensorflow-lite: cleanup dead code 2023-03-01 14:51:52 -08:00
Koushik Dutta
694c195024 rpc: fixup WeakRef typing 2023-03-01 14:51:40 -08:00
Koushik Dutta
c1f0281030 core: add finer grain user permissions 2023-03-01 14:51:14 -08:00
Brett Jia
fa218cbcbd remote: cleanup remote hint now that rebroadcast uses external by default (#594) 2023-03-01 14:18:42 -08:00
Koushik Dutta
a89700acc2 cli/client: decouple, upgrade packages, publish 2023-03-01 13:55:40 -08:00
Koushik Dutta
82fb24e275 rebroadcast: move url expansion into separate file 2023-03-01 12:22:33 -08:00
Koushik Dutta
eef67a9383 cli: fix arg parsing 2023-03-01 11:56:37 -08:00
Koushik Dutta
1180d9fa2c cli: rebuild 2023-03-01 11:51:36 -08:00
Koushik Dutta
57734f1d3c videoanalysis: remove extra settings 2023-02-28 23:56:39 -08:00
Koushik Dutta
dace750829 predict: publish 2023-02-28 21:53:33 -08:00
Koushik Dutta
f359a7167a server: nuke python prefix prior to install to purge old conflicting deps 2023-02-28 21:53:15 -08:00
Koushik Dutta
39c0759d1b tensorflow-lite: add simd support 2023-02-28 21:34:06 -08:00
Koushik Dutta
fee90334fb videoanalysis: snapshot mode cleanups 2023-02-28 20:48:31 -08:00
Koushik Dutta
80db6e50ab rebroadcast: fix external url behavior 2023-02-28 20:44:57 -08:00
Koushik Dutta
1fa6c2d842 tensorflow: reduce several expensice cpu resizes 2023-02-28 20:44:21 -08:00
Koushik Dutta
8b39c4c22c snapshot: fix debug file logging 2023-02-28 20:26:15 -08:00
Koushik Dutta
4b6fd5b5a8 server: remove debug logging 2023-02-28 20:20:17 -08:00
Koushik Dutta
f2d1909b6d docker: gstreamer vaapi is apparently xplat 2023-02-28 19:42:21 -08:00
Koushik Dutta
7917fb96dc docker: incude ffmpeg 2023-02-28 19:39:12 -08:00
Koushik Dutta
ad5fae98f1 docker: incude ffmpeg 2023-02-28 19:35:33 -08:00
Koushik Dutta
8412eb36fe rebroadcast: fix erroneous external check. 2023-02-28 11:22:51 -08:00
Koushik Dutta
822455383b rebroadcast: include error in warning message 2023-02-28 11:20:27 -08:00
Koushik Dutta
2d4357e4c0 server: preserve MediaObject name in constructor 2023-02-28 11:17:58 -08:00
Koushik Dutta
6336407f15 prepublish 2023-02-28 11:17:26 -08:00
Koushik Dutta
38e3492137 rebroadcast: use new rtsp code with auth and extenral access 2023-02-28 11:15:32 -08:00
Koushik Dutta
255e426e2d client: update 2023-02-28 11:10:47 -08:00
Koushik Dutta
fed1cf2a0d cli: update 2023-02-28 11:10:22 -08:00
Koushik Dutta
a5cb8c3fdc sdk: update 2023-02-28 11:09:07 -08:00
Koushik Dutta
514d86144f sdk: update 2023-02-28 11:07:47 -08:00
Koushik Dutta
21db7934c9 Merge branch 'main' of github.com:koush/scrypted 2023-02-28 08:16:00 -08:00
Koushik Dutta
14e4b5c0e3 rebroadcast/rtsp: initial support for clustering 2023-02-28 08:15:56 -08:00
Koushik Duta
6478ad0411 Merge branch 'main' of github.com:koush/scrypted 2023-02-27 22:55:45 -08:00
Koushik Duta
81b7d432e9 docker: add gstreamer vaapi decoder 2023-02-27 22:55:30 -08:00
Koushik Dutta
dcbf5094f9 Merge branch 'main' of github.com:koush/scrypted 2023-02-27 22:38:42 -08:00
Koushik Dutta
69ba181dfa docker: add intel vaapi compose example 2023-02-27 22:38:35 -08:00
Koushik Duta
88654275c1 hikvision: trim deps 2023-02-27 22:27:23 -08:00
Koushik Duta
62f28271ed sdk: disable link on build 2023-02-27 22:25:27 -08:00
Koushik Dutta
0538130e78 amcrest: publish support for multiple call buttons on dahua models 2023-02-27 13:28:09 -08:00
KoljaV
95a3a16227 amcrest: New DahuaEventType and introduction of CallerId for Dahua Intercoms with multiple Bell Buttons. (#592)
* Update main.ts

Some Dahua Intercom support multiple Call Buttons. Therefore a CallerID is introduced. Plus a new event (DahuaCallDeny) that resets the ring status.

* Update amcrest-api.ts

New Dahua Event Type for Call Deny.

* tested multiple times and sorted out two bugs

---------

Co-authored-by: Kolja Vornholt <kvornholt@MBP-von-Kolja.lan>
2023-02-27 13:27:34 -08:00
Koushik Dutta
bfbf89ff69 Merge branch 'main' of github.com:koush/scrypted 2023-02-27 13:20:36 -08:00
Koushik Dutta
e630589489 cameras: auto detect two way audio 2023-02-27 13:19:32 -08:00
Alex Leeds
99d1e51f36 ring: add support for various sensor types (#591)
* ring: add support for various sensor types

* ring: bump version
2023-02-27 10:52:50 -08:00
Koushik Dutta
ab42ccd889 cameras: publish 2023-02-26 23:21:47 -08:00
Koushik Dutta
767af25aa0 cameras: create probe utils 2023-02-26 22:09:30 -08:00
Koushik Dutta
7575dd82ce onvif: unescape xml strings 2023-02-26 16:52:32 -08:00
Koushik Dutta
9307bbd09e Merge branch 'main' of github.com:koush/scrypted 2023-02-26 14:21:59 -08:00
Koushik Dutta
68a9ec09e6 docker: readd google gpg keys that were failing 2023-02-26 14:21:40 -08:00
Brett Jia
f8a548401f remote: allow doorbells and filter out irrelevant plugins (#588) 2023-02-26 11:44:47 -08:00
Koushik Dutta
26d1f8e58c cameras: include ip in device info 2023-02-26 11:42:52 -08:00
Koushik Dutta
8772e25c8e sdk/client: update 2023-02-26 11:16:28 -08:00
Koushik Dutta
378ac82c8c cameras: refresh device info on startup 2023-02-26 09:54:25 -08:00
Koushik Dutta
fcb1292ffd Update Dockerfile.full.header 2023-02-25 19:17:24 -08:00
Brett Jia
18112ee40f remote: allow API type and use ObjectDetection (#586)
* include implementation from standalone repo

* simplify monkeypatching

* allow API types and use ObjectDetection interface
2023-02-25 19:00:57 -08:00
Brett Jia
fa8b9dfe99 remote: Scrypted remote plugin (#585)
* include implementation from standalone repo

* simplify monkeypatching
2023-02-25 18:46:33 -08:00
Alex Leeds
e7dff4edc9 add fan support to nest thermostats (#583)
* add fan support to nest thermostats

* fix fan refresh
2023-02-25 17:58:23 -08:00
Koushik Dutta
7614d12363 prepublish 2023-02-25 14:25:24 -08:00
Koushik Dutta
3189317b2d server: additional admin login traps for HA ingress 2023-02-25 14:25:16 -08:00
Koushik Dutta
410d11248f snapshot: improve failure resiliency with prebuffer fallback and ignoring ffpmeg errors when buffers are returned 2023-02-25 13:41:41 -08:00
Koushik Dutta
28e1f5ac8a prepublish 2023-02-25 09:55:03 -08:00
Koushik Dutta
bafe73d296 server: add support for admin configuration via env 2023-02-25 09:54:54 -08:00
Koushik Dutta
f17ce50f17 webrtc: werift config should also respect turn server preference 2023-02-25 09:33:09 -08:00
Koushik Dutta
18f5872be1 core: fix oauth on rehosted urls 2023-02-24 22:00:48 -08:00
Koushik Dutta
fdccaaa65e client: publish 2023-02-24 19:57:45 -08:00
Koushik Dutta
6a55172924 server/rpc: add support for clobbering non-state properties and methods 2023-02-24 19:41:50 -08:00
Koushik Dutta
1e41af77fa rebroadcast: add external url request support 2023-02-24 18:58:28 -08:00
Koushik Dutta
e169a6e02d rebroadcast: add rtsp path to prevent port guessing access 2023-02-24 18:49:16 -08:00
Koushik Dutta
ef55c834af sdk: getVideoStream routing hints 2023-02-24 18:48:39 -08:00
Koushik Dutta
3812ad92ac core: fix safari weirdness on async methods and window open 2023-02-24 16:26:07 -08:00
Koushik Dutta
0bdb402e7b core: use target _blank when starting oauth from inside iframe 2023-02-24 16:11:38 -08:00
Koushik Dutta
1588ea250b server: publish rpc change 2023-02-24 16:01:52 -08:00
274 changed files with 13130 additions and 10283 deletions

9
.gitmodules vendored
View File

@@ -19,6 +19,7 @@
[submodule "external/ring-client-api"]
path = external/ring-client-api
url = ../../koush/ring
branch = fork
[submodule "plugins/vscode-typescript"]
path = plugins/vscode-typescript
url = ../../koush/scrypted-vscode-typescript/
@@ -28,20 +29,14 @@
[submodule "plugins/zwave/file-stream-rotator"]
path = plugins/zwave/file-stream-rotator
url = ../../koush/file-stream-rotator.git
[submodule "external/push-receiver"]
path = external/push-receiver
url = ../../koush/push-receiver.git
[submodule "sdk/developer.scrypted.app"]
path = sdk/developer.scrypted.app
url = ../../koush/developer.scrypted.app
[submodule "plugins/sample-cameraprovider"]
path = plugins/sample-cameraprovider
url = ../../koush/scrypted-sample-cameraprovider
[submodule "plugins/objectdetector/node-moving-things-tracker"]
path = plugins/objectdetector/node-moving-things-tracker
url = ../../koush/node-moving-things-tracker.git
[submodule "plugins/tensorflow-lite/sort_oh"]
path = plugins/tensorflow-lite/sort_oh
path = plugins/sort-tracker/sort_oh
url = ../../koush/sort_oh.git
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/node-nat-upnp

View File

@@ -2,14 +2,15 @@ import type { TranspileOptions } from "typescript";
import sdk, { ScryptedDeviceBase, MixinDeviceBase, ScryptedInterface, ScryptedDeviceType } from "@scrypted/sdk";
import vm from "vm";
import fs from 'fs';
import { newThread } from '@scrypted/server/src/threading';
import { ScriptDevice } from "./monaco/script-device";
import { ScryptedInterfaceDescriptors } from "@scrypted/sdk";
import fetch from 'node-fetch-commonjs';
import { PluginAPIProxy } from '../../../server/src/plugin/plugin-api';
import { SystemManagerImpl } from '../../../server/src/plugin/system';
const { systemManager, deviceManager, mediaManager, endpointManager } = sdk;
function tsCompile(source: string, options: TranspileOptions = null): string {
export async function tsCompile(source: string, options: TranspileOptions = null): Promise<string> {
const ts = require("typescript");
const { ScriptTarget } = ts;
@@ -25,27 +26,6 @@ function tsCompile(source: string, options: TranspileOptions = null): string {
return ts.transpileModule(source, options).outputText;
}
async function tsCompileThread(source: string, options: TranspileOptions = null): Promise<string> {
return newThread({
source, options,
customRequire: '__webpack_require__',
}, ({ source, options }) => {
const ts = global.require("typescript");
const { ScriptTarget } = ts;
// Default options -- you could also perform a merge, or use the project tsconfig.json
if (null === options) {
options = {
compilerOptions: {
target: ScriptTarget.ESNext,
module: ts.ModuleKind.CommonJS
}
};
}
return ts.transpileModule(source, options).outputText;
});
}
function getTypeDefs() {
const scryptedTypesDefs = fs.readFileSync('@types/sdk/types.d.ts').toString();
const scryptedIndexDefs = fs.readFileSync('@types/sdk/index.d.ts').toString();
@@ -61,14 +41,27 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
}, extraLibs);
const allScripts = Object.values(libs).join('\n').toString() + script;
let compiled: string;
const worker = sdk.fork<{
tsCompile: typeof tsCompile,
}>();
worker.worker.on('error', () => { })
try {
compiled = await tsCompileThread(allScripts);
const result = await worker.result;
compiled = await result.tsCompile(allScripts);
}
catch (e) {
device.log.e('Error compiling typescript.');
device.console.error(e);
throw e;
}
finally {
worker.worker.terminate();
}
const smProxy = new SystemManagerImpl();
smProxy.state = systemManager.getSystemState();
const apiProxy = new PluginAPIProxy(sdk.pluginHostAPI);
smProxy.api = apiProxy;
const allParams = Object.assign({}, params, {
sdk,
@@ -76,7 +69,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
fetch,
ScryptedDeviceBase,
MixinDeviceBase,
systemManager,
systemManager: smProxy,
deviceManager,
endpointManager,
mediaManager,
@@ -111,6 +104,7 @@ export async function scryptedEval(device: ScryptedDeviceBase, script: string, e
return {
value,
defaultExport,
apiProxy,
};
}
catch (e) {

View File

@@ -4,10 +4,10 @@ import { once } from 'events';
import { BASIC } from 'http-auth-utils/dist/index';
import { parseHTTPHeadersQuotedKeyValueSet } from 'http-auth-utils/dist/utils';
import net from 'net';
import { Duplex, Readable } from 'stream';
import { Duplex, Readable, Writable } from 'stream';
import tls from 'tls';
import { Deferred } from './deferred';
import { closeQuiet, createBindUdp, createBindZero } from './listen-cluster';
import { closeQuiet, createBindUdp, createBindZero, listenZeroSingleClient } from './listen-cluster';
import { timeoutPromise } from './promise-utils';
import { readLength, readLine } from './read-stream';
import { MSection, parseSdp } from './sdp-utils';
@@ -47,6 +47,29 @@ export async function readMessage(client: Readable): Promise<string[]> {
}
}
export async function readBody(client: Readable, response: Headers) {
const cl = parseInt(response['content-length']);
if (cl)
return readLength(client, cl)
}
export function writeMessage(client: Writable, messageLine: string, body: Buffer, headers: Headers, console?: Console) {
let message = messageLine !== undefined ? `${messageLine}\r\n` : '';
if (body)
headers['Content-Length'] = body.length.toString();
for (const [key, value] of Object.entries(headers)) {
message += `${key}: ${value}\r\n`;
}
message += '\r\n';
client.write(message);
console?.log('rtsp outgoing message\n', message);
console?.log();
if (body)
client.write(body);
}
// https://yumichan.net/video-processing/video-compression/introduction-to-h264-nal-unit/
export const H264_NAL_TYPE_RESERVED0 = 0;
@@ -284,18 +307,7 @@ export class RtspBase {
}
write(messageLine: string, headers: Headers, body?: Buffer) {
let message = `${messageLine}\r\n`;
if (body)
headers['Content-Length'] = body.length.toString();
for (const [key, value] of Object.entries(headers)) {
message += `${key}: ${value}\r\n`;
}
message += '\r\n';
this.client.write(message);
this.console?.log('rtsp outgoing message\n', message);
this.console?.log();
if (body)
this.client.write(body);
writeMessage(this.client, messageLine, body, headers, this.console);
}
async readMessage(): Promise<string[]> {
@@ -590,9 +602,7 @@ export class RtspClient extends RtspBase {
}
async readBody(response: Headers) {
const cl = parseInt(response['content-length']);
if (cl)
return readLength(this.client, cl)
return readBody(this.client, response);
}
async request(method: string, headers?: Headers, path?: string, body?: Buffer, authenticating?: boolean): Promise<RtspServerResponse> {
@@ -1053,3 +1063,33 @@ export class RtspServer {
}
}
}
export async function listenSingleRtspClient<T extends RtspServer>(options?: {
hostname?: string,
pathToken?: string,
createServer?(duplex: Duplex): T,
}) {
const pathToken = options?.pathToken || crypto.randomBytes(8).toString('hex');
let { url, clientPromise, server } = await listenZeroSingleClient(options?.hostname);
const rtspServerPath = '/' + pathToken;
url = url.replace('tcp:', 'rtsp:') + rtspServerPath;
const rtspServerPromise = clientPromise.then(client => {
const createServer = options?.createServer || (duplex => new RtspServer(duplex));
const rtspServer = createServer(client);
rtspServer.checkRequest = async (method, url, headers, message) => {
rtspServer.checkRequest = undefined;
const u = new URL(url);
return u.pathname === rtspServerPath;
};
return rtspServer as T;
});
return {
url,
rtspServerPromise,
server,
}
}

56
common/test/rtsp-proxy.ts Normal file
View File

@@ -0,0 +1,56 @@
import net from 'net';
import { listenZero } from '../src/listen-cluster';
import { RtspClient, RtspServer } from '../src/rtsp-server';
async function main() {
const server = net.createServer(async serverSocket => {
const client = new RtspClient('rtsp://localhost:57594/911db962087f904d');
await client.options();
const describeResponse = await client.describe();
const sdp = describeResponse.body.toString();
const server = new RtspServer(serverSocket, sdp, true);
const setupResponse = await server.handlePlayback();
if (setupResponse !== 'play') {
serverSocket.destroy();
client.client.destroy();
return;
}
console.log('playback handled');
let channel = 0;
for (const track of Object.keys(server.setupTracks)) {
const setupTrack = server.setupTracks[track];
await client.setup({
// type: 'udp',
type: 'tcp',
port: channel,
path: setupTrack.control,
onRtp(rtspHeader, rtp) {
server.sendTrack(setupTrack.control, rtp, false);
},
});
channel += 2;
}
await client.play();
console.log('client playing');
await client.readLoop();
});
let port: number;
if (false) {
port = await listenZero(server);
}
else {
port = 5555;
server.listen(5555)
}
console.log(`rtsp://127.0.0.1:${port}`);
}
main();

View File

@@ -31,20 +31,19 @@ RUN apt-get -y install \
build-essential \
cmake \
gcc \
gir1.2-gtk-3.0 \
libcairo2-dev \
libgirepository1.0-dev \
libglib2.0-dev \
libjpeg-dev \
libgif-dev \
libopenjp2-7 \
libpango1.0-dev \
librsvg2-dev \
pkg-config
pkg-config \
libvips
# ffmpeg
RUN apt-get -y install \
ffmpeg
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \
@@ -63,8 +62,10 @@ RUN apt-get -y install \
# python pip
RUN python3 -m pip install --upgrade pip
# pyvips is broken on x86 due to mismatch ffi
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
RUN pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
RUN python3 -m pip install dlib
################################################################
# End section generated from template/Dockerfile.full.header

View File

@@ -20,6 +20,11 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
# python native
RUN apt-get -y install \
python3 \

View File

@@ -19,6 +19,11 @@ RUN apt-get -y install \
libglib2.0-dev \
pkg-config
# ffmpeg
RUN apt-get -y install \
ffmpeg
ENV SCRYPTED_FFMPEG_PATH=ffmpeg
ENV SCRYPTED_DOCKER_SERVE="true"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"

View File

@@ -38,6 +38,8 @@ services:
# - /dev/ttyACM0:/dev/ttyACM0
# all usb devices, such as coral tpu
# - /dev/bus/usb:/dev/bus/usb
# intel hardware accelerated video decoding
# - /dev/dri:/dev/dri
volumes:
- ~/.scrypted/volume:/server/volume

View File

@@ -40,13 +40,14 @@ echo "Installing Scrypted dependencies..."
RUN_IGNORE xcode-select --install
RUN brew update
RUN_IGNORE brew install node@18
# needed by scrypted-ffmpeg
RUN_IGNORE brew install sdl2
# snapshot plugin and others
RUN brew install libvips
# gstreamer plugins
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly
# gst python bindings
RUN_IGNORE brew install gst-python
# python image library
# todo: consider removing this
RUN_IGNORE brew install pillow
### HACK WORKAROUND
@@ -102,7 +103,11 @@ then
fi
RUN python$PYTHON_VERSION -m pip install --upgrade pip
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions typing opencv-python psutil
if [ "$PYTHON_VERSION" != "3.10" ]
then
RUN python$PYTHON_VERSION -m pip install typing
fi
RUN python$PYTHON_VERSION -m pip install aiofiles debugpy typing_extensions opencv-python psutil
echo "Installing Scrypted Launch Agent..."

View File

@@ -26,21 +26,21 @@ RUN apt-get -y upgrade
# base development stuff
RUN apt-get -y install \
build-essential \
cmake \
gcc \
gir1.2-gtk-3.0 \
libcairo2-dev \
libgirepository1.0-dev \
libglib2.0-dev \
libjpeg-dev \
libgif-dev \
libopenjp2-7 \
libpango1.0-dev \
librsvg2-dev \
pkg-config
pkg-config \
libvips
# ffmpeg
RUN apt-get -y install \
ffmpeg
# gstreamer native https://gstreamer.freedesktop.org/documentation/installing/on-linux.html?gi-language=c#install-gstreamer-on-ubuntu-or-debian
RUN apt-get -y install \
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa
gstreamer1.0-tools libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \
@@ -59,6 +59,9 @@ RUN apt-get -y install \
# python pip
RUN python3 -m pip install --upgrade pip
# pyvips is broken on x86 due to mismatch ffi
# https://stackoverflow.com/questions/62658237/it-seems-that-the-version-of-the-libffi-library-seen-at-runtime-is-different-fro
RUN pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
################################################################

Submodule external/push-receiver deleted from d054e083d6

View File

@@ -6,7 +6,7 @@
"configurations": [
{
"console": "integratedTerminal",
"type": "pwa-node",
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
@@ -20,8 +20,10 @@
"ts-node/register"
],
"args": [
"install",
"@scrypted/google-device-access"
"ffplay",
"Kitchen",
"getRecordingStream",
"{\"startTime\":1677699495709}"
],
"sourceMaps": true,
"resolveSourceMapLocations": [

View File

@@ -1,119 +1,183 @@
{
"name": "scrypted",
"version": "1.0.57",
"lockfileVersion": 2,
"version": "1.0.67",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scrypted",
"version": "1.0.57",
"version": "1.0.67",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",
"adm-zip": "^0.5.9",
"@scrypted/client": "^1.1.43",
"@scrypted/types": "^0.2.66",
"adm-zip": "^0.5.10",
"axios": "^0.21.4",
"engine.io-client": "^5.2.0",
"ip": "^1.1.8",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"mkdirp": "^1.0.4",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tslib": "^2.3.1"
"semver": "^7.3.8",
"tslib": "^2.5.0"
},
"bin": {
"scrypted": "dist/packages/cli/src/main.js"
"scrypted": "dist/main.js"
},
"devDependencies": {
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.14.2",
"@types/readline-sync": "^1.4.4",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"ts-node": "^10.2.1",
"typescript": "^4.8.2"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"extraneous": 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"
},
"devDependencies": {
"@types/node": "^16.9.0"
}
},
"node_modules/@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true,
"engines": {
"node": ">= 12"
"@types/semver": "^7.3.13",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-consumer": "0.8.0"
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
"dev": true,
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@scrypted/client": {
"version": "1.1.43",
"resolved": "https://registry.npmjs.org/@scrypted/client/-/client-1.1.43.tgz",
"integrity": "sha512-qpeGdqFga/Fx51MoF3E0iBPCjE/SDEIVdGh8Ws5dqw38bxUJD264c9NsNyCguLKyYguErKTAWnQkzqhO0bUbaA==",
"dependencies": {
"@scrypted/types": "^0.2.66",
"axios": "^0.25.0",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
}
},
"node_modules/@scrypted/client/node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"dependencies": {
"follow-redirects": "^1.14.7"
}
},
"node_modules/@scrypted/client/node_modules/engine.io-client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/@scrypted/client/node_modules/engine.io-parser": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@scrypted/client/node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@scrypted/types": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
"version": "0.2.66",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.66.tgz",
"integrity": "sha512-AL2iD7OmpqZlQMlpZKUBHpzL7H1IHhwKOi9uhRbVwG7EIDwenTspqtziH2Hyu0+XeCLf+gN69uQB6Qlz+QPf9A=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
"integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
"dev": true,
"dependencies": {
"@types/minimatch": "*",
"@types/minimatch": "^5.1.2",
"@types/node": "*"
}
},
"node_modules/@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
"integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
"dev": true
},
"node_modules/@types/mkdirp": {
@@ -126,9 +190,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/@types/readline-sync": {
@@ -148,15 +212,15 @@
}
},
"node_modules/@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
"integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==",
"dev": true
},
"node_modules/acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -175,9 +239,9 @@
}
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"engines": {
"node": ">=6.0"
}
@@ -204,7 +268,7 @@
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==",
"engines": {
"node": ">= 0.6.0"
}
@@ -226,7 +290,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/create-require": {
"version": "1.1.1",
@@ -235,9 +299,9 @@
"dev": true
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -288,9 +352,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
@@ -306,25 +370,20 @@
}
}
},
"node_modules/fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -338,12 +397,12 @@
"node_modules/has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
"integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -359,11 +418,6 @@
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"node_modules/linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -381,17 +435,6 @@
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"node_modules/memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"dependencies": {
"fs-monkey": "1.0.3"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -422,7 +465,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
@@ -440,7 +483,7 @@
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
@@ -468,9 +511,9 @@
}
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -482,12 +525,12 @@
}
},
"node_modules/ts-node": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "0.7.0",
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
@@ -498,11 +541,13 @@
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
@@ -523,14 +568,14 @@
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/typescript": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -540,10 +585,16 @@
"node": ">=4.2.0"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "7.4.6",
@@ -581,7 +632,7 @@
"node_modules/yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
"integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg=="
},
"node_modules/yn": {
"version": "3.1.1",
@@ -592,413 +643,5 @@
"node": ">=6"
}
}
},
"dependencies": {
"@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"dev": true
},
"@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"dev": true,
"requires": {
"@cspotcode/source-map-consumer": "0.8.0"
}
},
"@scrypted/types": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.0.6.tgz",
"integrity": "sha512-r/attybPcJvBNll3g+k8i2jQwQiu0izoBazZ+Kvsdeayr3Mbzm1NaBkwbUPICroWJKY+jlfoaZSQt4eGTX+vog=="
},
"@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==",
"dev": true
},
"@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==",
"dev": true
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"dev": true
},
"@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==",
"dev": true
},
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
"dev": true,
"requires": {
"@types/minimatch": "*",
"@types/node": "*"
}
},
"@types/minimatch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
"dev": true
},
"@types/mkdirp": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.2.tgz",
"integrity": "sha512-o0K1tSO0Dx5X6xlU5F1D6625FawhC3dU3iqr25lluNv/+/QIVH8RLNEiVokgIZo+mz+87w/3Mkg/VvQS+J51fQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==",
"dev": true
},
"@types/readline-sync": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@types/readline-sync/-/readline-sync-1.4.4.tgz",
"integrity": "sha512-cFjVIoiamX7U6zkO2VPvXyTxbFDdiRo902IarJuPVxBhpDnXhwSaVE86ip+SCuyWBbEioKCkT4C88RNTxBM1Dw==",
"dev": true
},
"@types/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==",
"dev": true,
"requires": {
"@types/glob": "*",
"@types/node": "*"
}
},
"@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"dev": true
},
"acorn": {
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz",
"integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==",
"dev": true
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"dev": true
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"engine.io-client": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.2.0.tgz",
"integrity": "sha512-BcIBXGBkT7wKecwnfrSV79G2X5lSUSgeAGgoo60plXf8UsQEvCQww/KMwXSMhVjb98fFYNq20CC5eo8IOAPqsg==",
"requires": {
"base64-arraybuffer": "0.1.4",
"component-emitter": "~1.3.0",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.1",
"has-cors": "1.1.0",
"parseqs": "0.0.6",
"parseuri": "0.0.6",
"ws": "~7.4.2",
"xmlhttprequest-ssl": "~2.0.0",
"yeast": "0.1.2"
}
},
"engine.io-parser": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
"requires": {
"base64-arraybuffer": "0.1.4"
}
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"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"
}
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"requires": {
"fs-monkey": "1.0.3"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"parseqs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz",
"integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w=="
},
"parseuri": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz",
"integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"readline-sync": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"requires": {
"lru-cache": "^6.0.0"
}
},
"ts-node": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.3.0.tgz",
"integrity": "sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw==",
"dev": true,
"requires": {
"@cspotcode/source-map-support": "0.7.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"yn": "3.1.1"
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"typescript": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

View File

@@ -1,10 +1,10 @@
{
"name": "scrypted",
"version": "1.0.57",
"version": "1.0.67",
"description": "",
"main": "./dist/packages/cli/src/main.js",
"main": "./dist/main.js",
"bin": {
"scrypted": "./dist/packages/cli/src/main.js"
"scrypted": "./dist/main.js"
},
"scripts": {
"prebuild": "rimraf dist",
@@ -16,25 +16,25 @@
"author": "",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.0.6",
"adm-zip": "^0.5.9",
"@scrypted/client": "^1.1.43",
"@scrypted/types": "^0.2.66",
"adm-zip": "^0.5.10",
"axios": "^0.21.4",
"engine.io-client": "^5.2.0",
"ip": "^1.1.8",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"mkdirp": "^1.0.4",
"readline-sync": "^1.4.10",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tslib": "^2.3.1"
"semver": "^7.3.8",
"tslib": "^2.5.0"
},
"devDependencies": {
"@types/mkdirp": "^1.0.2",
"@types/node": "^18.14.2",
"@types/readline-sync": "^1.4.4",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.9",
"ts-node": "^10.2.1",
"typescript": "^4.8.2"
"@types/semver": "^7.3.13",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
}

View File

@@ -7,11 +7,15 @@ import readline from 'readline-sync';
import https from 'https';
import mkdirp from 'mkdirp';
import { installServe, serveMain } from './service';
import { connectScryptedClient } from '../../client/src/index';
import { ScryptedMimeTypes, FFMpegInput } from '@scrypted/types';
import { connectScryptedClient } from '@scrypted/client';
import { ScryptedMimeTypes, FFmpegInput } from '@scrypted/types';
import semver from 'semver';
import child_process from 'child_process';
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
if (!semver.gte(process.version, '16.0.0')) {
throw new Error('"node" version out of date. Please update node to v16 or higher.')
}
@@ -57,6 +61,7 @@ async function doLogin(host: string) {
password,
},
url,
httpsAgent,
}, axiosConfig));
mkdirp.sync(scryptedHome);
@@ -112,13 +117,16 @@ async function runCommand() {
pluginId: '@scrypted/core',
username: login.username,
password: login.token,
axiosConfig: {
httpsAgent,
}
});
const device: any = sdk.systemManager.getDeviceById(idOrName) || sdk.systemManager.getDeviceByName(idOrName);
if (!device)
throw new Error('device not found: ' + idOrName);
const method = process.argv[4];
const args = process.argv.slice(5).map(arg => () => {
const args = process.argv.slice(5).map(arg => {
try {
return JSON.parse(arg);
}
@@ -157,9 +165,15 @@ async function main() {
}
else if (process.argv[2] === 'ffplay') {
const { sdk, pendingResult } = await runCommand();
const ffinput = await sdk.mediaManager.convertMediaObjectToJSON<FFMpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
console.log(ffinput);
child_process.spawn('ffplay', ffinput.inputArguments, {
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(await pendingResult, ScryptedMimeTypes.FFmpegInput);
if (ffmpegInput.url && ffmpegInput.urls?.[0]) {
const url = new URL(ffmpegInput.url);
if (url.hostname === '127.0.0.1' && ffmpegInput.urls?.[0]) {
ffmpegInput.inputArguments = ffmpegInput.inputArguments.map(i => i === ffmpegInput.url ? ffmpegInput.urls?.[0] : i);
}
}
console.log('ffplay', ...ffmpegInput.inputArguments);
child_process.spawn('ffplay', ffmpegInput.inputArguments, {
stdio: 'inherit',
});
sdk.disconnect();

View File

@@ -1,15 +1,16 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"module": "commonjs",
"target": "ESNext",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
"sourceMap": true,
"declaration": true
"module": "commonjs",
"target": "esnext",
"noImplicitAny": true,
"outDir": "./dist",
"esModuleInterop": true,
"sourceMap": true,
"inlineSources": true,
"declaration": true,
"resolveJsonModule": true,
},
"include": [
"src/**/*"
"src/**/*"
],
}

View File

@@ -1,77 +1,35 @@
{
"name": "@scrypted/client",
"version": "1.1.37",
"lockfileVersion": 2,
"version": "1.1.43",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.43",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"adm-zip": "^0.5.9",
"@scrypted/types": "^0.2.76",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
}
},
"../../common": {
"name": "@scrypted/common",
"version": "1.0.1",
"extraneous": 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"
},
"devDependencies": {
"@types/node": "^16.9.0"
}
},
"../../sdk/types": {
"name": "@scrypted/types",
"version": "0.0.9",
"extraneous": true,
"license": "ISC",
"devDependencies": {}
},
"../common": {
"extraneous": true
},
"../sdk/types": {
"extraneous": true
},
"node_modules/@scrypted/types": {
"version": "0.2.64",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
"version": "0.2.76",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.76.tgz",
"integrity": "sha512-/7n8ICkXj8TGba4cHvckLCgSNsOmOGQ8I+Jd8fX9sxkthgsZhF5At8PHhHdkCDS+yfSmfXHkcqluZZOfYPkpAg=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@types/adm-zip": {
"version": "0.4.34",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
@@ -82,19 +40,11 @@
}
},
"node_modules/@types/node": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
"engines": {
"node": ">=6.0"
}
},
"node_modules/axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
@@ -120,12 +70,12 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -139,29 +89,29 @@
}
},
"node_modules/engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
@@ -177,25 +127,20 @@
}
}
},
"node_modules/fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@@ -209,7 +154,7 @@
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -220,22 +165,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"node_modules/memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"dependencies": {
"fs-monkey": "1.0.3"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -255,7 +184,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
@@ -263,7 +192,7 @@
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
@@ -283,9 +212,9 @@
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -298,12 +227,12 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
@@ -328,209 +257,5 @@
"node": ">=0.4.0"
}
}
},
"dependencies": {
"@scrypted/types": {
"version": "0.2.64",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
},
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@types/adm-zip": {
"version": "0.4.34",
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/ip": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
"dev": true
},
"adm-zip": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
},
"axios": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
"requires": {
"follow-redirects": "^1.14.7"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"requires": {
"ms": "2.1.2"
}
},
"engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
},
"fs-monkey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"linkfs": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
},
"memfs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
"requires": {
"fs-monkey": "1.0.3"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"requires": {
"glob": "^7.1.3"
}
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.43",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -12,18 +12,14 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"typescript": "^4.7.4"
"@types/node": "^18.14.2",
"typescript": "^4.9.5"
},
"dependencies": {
"@scrypted/types": "^0.2.64",
"adm-zip": "^0.5.9",
"@scrypted/types": "^0.2.76",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import { RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import { MediaObjectOptions, RTCConnectionManagement, RTCSignalingSession, ScryptedStatic } from "@scrypted/types";
import axios, { AxiosRequestConfig } from 'axios';
import * as eio from 'engine.io-client';
import { SocketOptions } from 'engine.io-client';
@@ -49,6 +49,7 @@ export interface ScryptedClientStatic extends ScryptedStatic {
connectionType: ScryptedClientConnectionType;
authorization?: string;
queryToken?: { [parameter: string]: string };
rpcPeer: RpcPeer,
}
export interface ScryptedConnectionOptions {
@@ -503,7 +504,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
} = scrypted;
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async (data, mimeType, options) => {
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
const mo: MediaObjectRemote & {
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
@@ -519,7 +520,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
return data;
},
};
return mo;
return mo as any;
}
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
@@ -587,6 +588,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
browserSignalingSession,
authorization,
queryToken,
rpcPeer,
}
socket.on('close', () => {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"license": "ISC",
"devDependencies": {
"@types/node": "^18.11.18",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/rpc",
"version": "0.0.6",
"version": "0.0.7",
"description": "",
"main": "dist/index.js",
"scripts": {

View File

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

View File

@@ -1,175 +1,106 @@
{
"name": "@scrypted/alexa",
"version": "0.0.20",
"lockfileVersion": 2,
"version": "0.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/alexa",
"version": "0.0.20",
"version": "0.2.3",
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^0.24.0",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/server": "file:../../server"
}
},
"../../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"
},
"devDependencies": {
"@types/node": "^16.9.0"
"@scrypted/sdk": "../../sdk",
"@types/node": "^18.4.2"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.39",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"typescript": "^4.9.3",
"webpack": "^5.74.0",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.9",
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
}
},
"../../server": {
"version": "0.4.9",
"dev": true,
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.36",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.2.0",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^6.0.1",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^3.4.7",
"mime": "^3.0.0",
"mkdirp": "^1.0.4",
"nan": "^2.17.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^8.4.1",
"router": "^1.3.7",
"semver": "^7.3.8",
"source-map-support": "^0.5.21",
"tar": "^6.1.11",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"whatwg-mimetype": "^2.3.0",
"ws": "^8.9.0"
},
"bin": {
"scrypted-serve": "bin/scrypted-serve"
},
"devDependencies": {
"@types/adm-zip": "^0.4.34",
"@types/cookie-parser": "^1.4.3",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.14",
"@types/http-auth": "^4.1.1",
"@types/ip": "^1.1.0",
"@types/lodash": "^4.14.186",
"@types/mime": "^3.0.1",
"@types/mkdirp": "^1.0.2",
"@types/node-dijkstra": "^2.5.3",
"@types/node-forge": "^1.3.0",
"@types/pem": "^1.9.6",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.12",
"@types/source-map-support": "^0.5.6",
"@types/tar": "^4.0.5",
"@types/whatwg-mimetype": "^2.1.1",
"@types/ws": "^7.4.7"
},
"optionalDependencies": {
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
}
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
"link": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
},
"node_modules/@scrypted/server": {
"resolved": "../../server",
"link": true
},
"node_modules/@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
"version": "18.14.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
"dev": true
},
"node_modules/alexa-smarthome-ts": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/alexa-smarthome-ts/-/alexa-smarthome-ts-0.0.1.tgz",
"integrity": "sha512-Pbbs/fJ/2P/AN6f6/5UCH6WhW+HP3z9FtXpcuRgBI+WpT9dru9kYt/HiBeihmTPvvwmHMqKSCp0yodMqRJ2Zhw=="
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.14.4"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
@@ -185,6 +116,43 @@
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
@@ -193,124 +161,5 @@
"uuid": "dist/bin/uuid"
}
}
},
"dependencies": {
"@scrypted/common": {
"version": "file:../../common",
"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"
}
},
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^18.11.9",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21",
"typescript": "^4.9.3",
"webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@scrypted/server": {
"version": "file:../../server",
"requires": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"@scrypted/types": "^0.2.36",
"@types/adm-zip": "^0.4.34",
"@types/cookie-parser": "^1.4.3",
"@types/debug": "^4.1.7",
"@types/express": "^4.17.14",
"@types/http-auth": "^4.1.1",
"@types/ip": "^1.1.0",
"@types/lodash": "^4.14.186",
"@types/mime": "^3.0.1",
"@types/mkdirp": "^1.0.2",
"@types/node-dijkstra": "^2.5.3",
"@types/node-forge": "^1.3.0",
"@types/pem": "^1.9.6",
"@types/rimraf": "^3.0.2",
"@types/semver": "^7.3.12",
"@types/source-map-support": "^0.5.6",
"@types/tar": "^4.0.5",
"@types/whatwg-mimetype": "^2.1.1",
"@types/ws": "^7.4.7",
"adm-zip": "^0.5.9",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"engine.io": "^6.2.0",
"express": "^4.18.2",
"ffmpeg-static": "^5.1.0",
"http-auth": "^4.2.0",
"ip": "^1.1.8",
"level": "^6.0.1",
"linkfs": "^2.1.0",
"lodash": "^4.17.21",
"memfs": "^3.4.7",
"mime": "^3.0.0",
"mkdirp": "^1.0.4",
"nan": "^2.17.0",
"node-dijkstra": "^2.5.0",
"node-forge": "^1.3.1",
"node-gyp": "^8.4.1",
"node-pty-prebuilt-multiarch": "^0.10.1-pre.5",
"router": "^1.3.7",
"semver": "^7.3.8",
"source-map-support": "^0.5.21",
"tar": "^6.1.11",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"whatwg-mimetype": "^2.3.0",
"ws": "^8.9.0"
}
},
"@types/node": {
"version": "16.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
},
"alexa-smarthome-ts": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/alexa-smarthome-ts/-/alexa-smarthome-ts-0.0.1.tgz",
"integrity": "sha512-Pbbs/fJ/2P/AN6f6/5UCH6WhW+HP3z9FtXpcuRgBI+WpT9dru9kYt/HiBeihmTPvvwmHMqKSCp0yodMqRJ2Zhw=="
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"follow-redirects": {
"version": "1.14.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/alexa",
"version": "0.1.0",
"version": "0.2.3",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
@@ -21,11 +21,12 @@
"amazon"
],
"scrypted": {
"name": "Alexa Plugin",
"name": "Alexa",
"type": "API",
"interfaces": [
"HttpRequestHandler",
"MixinProvider"
"MixinProvider",
"Settings"
],
"pluginDependencies": [
"@scrypted/cloud",
@@ -33,14 +34,11 @@
]
},
"dependencies": {
"@types/node": "^16.6.1",
"alexa-smarthome-ts": "^0.0.1",
"axios": "^1.3.4",
"uuid": "^9.0.0"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/server": "file:../../server"
"@types/node": "^18.4.2",
"@scrypted/sdk": "../../sdk"
}
}

221
plugins/alexa/src/alexa.ts Normal file
View File

@@ -0,0 +1,221 @@
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';
/*
COMMON DIRECTIVES AND RESPONSES
*/
export interface AddOrUpdateReport {
event: {
header: Header<"Alexa.Discovery", "AddOrUpdateReport">;
payload: AddOrUpdateReportPayload;
}
}
export interface DeleteReport {
event: {
header: Header<"Alexa.Discovery", "DeleteReport">;
payload: DeleteReportPayload;
}
}
export interface StateReport extends Report<"Alexa", "StateReport"> { }
export interface ChangeReport extends Report<"Alexa", "ChangeReport", ChangePayload> { }
export interface Response {
event: Event<"Alexa", "Response">;
context?: Context;
}
export interface DeferredResponse {
event: Event<"Alexa", "DeferredResponse", DeferredPayload>;
}
export interface ErrorResponse {
event: Event<"Alexa", "ErrorResponse", ErrorPayload>;
}
/*
DEVICE EVENTS
*/
export interface WebRTCAnswerGeneratedForSessionEvent extends Report<"Alexa.RTCSessionController", "AnswerGeneratedForSession", WebRTCAnswerGeneratedForSessionPayload> { }
export interface WebRTCSessionConnectedEvent extends Report<"Alexa.RTCSessionController", "SessionConnected", WebRTCSessionPayload> { }
export interface WebRTCSessionDisconnectedEvent extends Report<"Alexa.RTCSessionController", "SessionDisconnected", WebRTCSessionPayload> { }
export interface ObjectDetectionEvent extends Report<"Alexa.SmartVision.ObjectDetectionSensor", "ObjectDetection", ObjectDetectionPayload> { }
export interface DoorbellPressEvent extends Report<"Alexa.DoorbellEventSource", "DoorbellPress", DoorbellPressPayload> { }
/*
IMPLIMENTATION TYPES
*/
export interface Header<NS = string, N = string> {
namespace: NS;
name: N;
messageId: string;
correlationToken?: string;
payloadVersion: string;
}
export interface Scope {
type: string;
token: string;
partition?: string;
userId?: string;
}
export interface Endpoint {
endpointId: string;
scope?: Scope;
cookie?: any;
}
export interface Payload { }
export interface Directive<NS = string, N = string, P = Payload> {
header: Header<NS, N>;
endpoint: Endpoint;
payload: P;
}
export interface Event<NS = string, N = string, P = Payload> {
header: Header<NS, N>;
endpoint: Endpoint;
payload: P;
}
export interface Property {
namespace: string;
instance?: string;
name: string;
value: any;
timeOfSample: string;
uncertaintyInMilliseconds?: number;
}
export interface Context {
properties: Property[];
}
export interface Report<NS = string, N = string, P = Payload> {
event: Event<NS, N, P>;
context: Context;
}
export interface DeferredPayload {
estimatedDeferralInSeconds: number;
}
export interface ErrorPayload {
type: string;
message: string;
}
export interface ChangePayload {
change: {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION" | "RULE_TRIGGER";
},
properties: Property[];
}
}
export interface WebRTCSessionPayload {
sessionId: string;
}
export interface WebRTCAnswerGeneratedForSessionPayload {
answer: {
format: string;
value: string;
}
}
export interface ObjectDetectionPayloadEvent {
eventIdenifier: string;
imageNetClass: string;
timeOfSample: string;
uncertaintyInMilliseconds: number;
objectIdentifier: string;
frameImageUri: string;
croppedImageUri: string;
}
export interface ObjectDetectionPayload {
events: ObjectDetectionPayloadEvent[]
}
export interface DoorbellPressPayload {
cause: {
type: "APP_INTERACTION" | "PERIODIC_POLL" | "PHYSICAL_INTERACTION" | "VOICE_INTERACTION";
},
timestamp: string;
}
export interface DiscoveryProperty {
supported: any[];
proactivelyReported: boolean;
retrievable: boolean;
}
export interface DiscoveryCapability {
type: string;
interface: string;
instance?: string;
version: string;
properties?: DiscoveryProperty;
capabilityResources?: any;
configuration?: any;
semantics?: any;
}
export interface DiscoveryEndpoint {
endpointId: string;
manufacturerName: string;
description: string;
friendlyName: string;
displayCategories: DisplayCategory[];
additionalAttributes?: {
"manufacturer"?: string;
"model"?: string;
"serialNumber"?: string;
"firmwareVersion"? : string;
"softwareVersion"?: string;
"customIdentifier"?: string;
};
capabilities?: DiscoveryCapability[];
connections?: any[];
relationships?: any;
cookie?: any;
}
export interface DiscoverPayload {
endpoints: DiscoveryEndpoint[]
}
export interface Discovery {
event: {
header: Header<"Alexa.Discovery", "Discover.Response">;
payload: DiscoverPayload;
}
}
export interface AddOrUpdateReportPayload {
endpoints: DiscoveryEndpoint[]
scope: Scope;
}
export interface DeleteReportEndpoint {
endpointId: string;
}
export interface DeleteReportPayload {
endpoints: DeleteReportEndpoint[]
scope: Scope;
}

131
plugins/alexa/src/common.ts Normal file
View File

@@ -0,0 +1,131 @@
import { Battery, Online, PowerSensor, ScryptedDevice, ScryptedInterface, HttpResponse } from "@scrypted/sdk";
import { v4 as createMessageId } from 'uuid';
export interface AlexaHttpResponse extends HttpResponse {
send(body: any, options?: any): void;
}
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
if (!device.interfaces.includes(ScryptedInterface.Online))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": device.online ? "OK" : "UNREACHABLE",
"reason": device.online ? undefined : "INTERNET_UNREACHABLE"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
if (!device.interfaces.includes(ScryptedInterface.Battery))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
const lowPower = device.batteryLevel < 20;
let health = undefined;
if (lowPower) {
health = {
"state": "WARNING",
"reasons": [
"LOW_CHARGE"
]
};
}
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "battery",
"value": {
health,
"levelPercentage": device.batteryLevel,
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function authErrorResponse(errorType: string, errorMessage: string, directive: any): any {
const { header } = directive;
const data = {
"event": {
header,
"payload": {
"type": errorType,
"message": errorMessage
}
}
};
data.event.header.name = "ErrorResponse";
data.event.header.messageId = createMessageId();
return data;
}
// https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-errorresponse.html#error-types
export function deviceErrorResponse (errorType: string, errorMessage: string, directive: any): any{
const { header, endpoint } = directive;
const data = {
"event": {
header,
endpoint,
"payload": {
"type": errorType,
"message": errorMessage
}
}
};
data.event.header.name = "ErrorResponse";
data.event.header.messageId = createMessageId();
return data;
}
export function mirroredResponse (directive: any): any {
const { header, endpoint, payload } = directive;
const data = {
"event": {
header,
endpoint,
payload
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
return data;
}
export function sendDeviceResponse(data: any, response: any, device: ScryptedDevice) {
data = addBattery(data, device);
data = addOnline(data, device);
response.send(data);
}

View File

@@ -0,0 +1,34 @@
import { HttpRequest, ScryptedDevice } from "@scrypted/sdk";
import { AlexaHttpResponse, sendDeviceResponse } from "./common";
import { supportedTypes } from "./types";
import { v4 as createMessageId } from 'uuid';
import { Directive, StateReport } from "./alexa";
export type AlexaHandler = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive) => Promise<void>
export type AlexaDeviceHandler<T> = (request: HttpRequest, response: AlexaHttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
export const alexaDeviceHandlers = new Map<string, AlexaDeviceHandler<any>>();
export const alexaHandlers = new Map<string, AlexaHandler>();
alexaDeviceHandlers.set('Alexa/ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const report = await supportedType.sendReport(device);
let data = {
"event": {
header,
endpoint,
payload
},
context: report?.context
} as StateReport;
data.event.header.name = "StateReport";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
});

View File

@@ -1,18 +1,21 @@
import axios from 'axios';
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Setting, SettingValue, Settings } from '@scrypted/sdk';
import sdk, { HttpRequest, HttpRequestHandler, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, EventDetails, Setting, SettingValue, Settings, HttpResponseOptions, HttpResponse } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { AutoenableMixinProvider } from '@scrypted/common/src/autoenable-mixin-provider';
import { isSupported } from './types';
import { DiscoveryEndpoint, DiscoverEvent } from 'alexa-smarthome-ts';
import { AlexaHandler, addBattery, addOnline, addPowerSensor, capabilityHandlers, supportedTypes } from './types/common';
import { createMessageId } from './message';
import { addBattery, addOnline, deviceErrorResponse, mirroredResponse, authErrorResponse, AlexaHttpResponse } from './common';
import { supportedTypes } from './types';
import { v4 as createMessageId } from 'uuid';
import { ChangeReport, Discovery, DiscoveryEndpoint } from './alexa';
import { alexaHandlers, alexaDeviceHandlers } from './handlers';
const { systemManager, deviceManager } = sdk;
const client_id = "amzn1.application-oa2-client.3283807e04d8408eb44a698c10f9dd13";
const client_secret = "bed445e2b26730acd818b90e175b275f6b67b18ff8645e571c5b3e311fa75ee9";
const includeToken = 4;
class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler, MixinProvider, Settings {
export let DEBUG = false;
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
hide: true,
@@ -22,6 +25,10 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
multiple: true,
hide: true
},
defaultIncluded: {
hide: true,
json: true
},
apiEndpoint: {
title: 'Alexa Endpoint',
description: 'This is the endpoint Alexa will use to send events to. This is set after you login.',
@@ -30,88 +37,169 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
});
handlers = new Map<string, AlexaHandler>();
accessToken: Promise<string>;
validAuths = new Set<string>();
devices = new Map<string, ScryptedDevice>();
constructor(nativeId?: string) {
super(nativeId);
this.handlers.set('Alexa.Authorization', this.alexaAuthorization);
this.handlers.set('Alexa.Discovery', this.alexaDiscovery);
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
this.syncDevices();
this.start();
}
systemManager.listen(async (eventSource, eventDetails, eventData) => {
if (!eventSource)
return;
async start() {
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
await this.tryEnableMixin(device);
}
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType) {
this.console.warn(`${eventSource.name} no longer supported type?`);
return;
systemManager.listen((async (eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) => {
const status = await this.tryEnableMixin(eventSource);
// sync new devices when added or removed
if (status === DeviceMixinStatus.Setup)
await this.syncEndpoints();
if (status === DeviceMixinStatus.Setup || status === DeviceMixinStatus.AlreadySetup) {
if (!this.devices.has(eventSource.id)) {
this.devices.set(eventSource.id, eventSource);
eventSource.listen(ScryptedInterface.ObjectDetector, this.deviceListen.bind(this));
}
this.deviceListen(eventSource, eventDetails, eventData);
}
}).bind(this));
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
let data = {
"event": {
"header": {
"messageId": createMessageId(),
"namespace": report?.namespace ?? "Alexa",
"name": report?.name ?? "ChangeReport",
"payloadVersion": "3"
},
"endpoint": {
"endpointId": eventSource.id,
"scope": undefined
},
"payload": report?.payload,
await this.syncEndpoints();
}
private async tryEnableMixin(device: ScryptedDevice): Promise<DeviceMixinStatus> {
if (!device)
return DeviceMixinStatus.NotSupported;
const mixins = (device.mixins || []).slice();
if (mixins.includes(this.id))
return DeviceMixinStatus.AlreadySetup;
const defaultIncluded = this.storageSettings.values.defaultIncluded || {};
if (defaultIncluded[device.id] === includeToken)
return DeviceMixinStatus.AlreadySetup;
if (!supportedTypes.has(device.type))
return DeviceMixinStatus.NotSupported;
mixins.push(this.id);
const plugins = await systemManager.getComponent('plugins');
await plugins.setMixins(device.id, mixins);
defaultIncluded[device.id] = includeToken;
this.storageSettings.values.defaultIncluded = defaultIncluded;
return DeviceMixinStatus.Setup;
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
const available = supportedTypes.has(type);
if (available)
return [];
return;
}
async getMixin(device: ScryptedDevice, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any }): Promise<any> {
return device;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
const device = systemManager.getDeviceById(id);
const mixins = (device.mixins || []).slice();
if (mixins.includes(this.id))
return;
this.log.i(`Device removed from Alexa: ${device.name}. Requesting sync.`);
await this.syncEndpoints();
}
async deviceListen(eventSource: ScryptedDevice | undefined, eventDetails: EventDetails, eventData: any) : Promise<void> {
if (!eventSource)
return;
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice)
return;
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType)
return;
const report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
let data = {
"event": {
"header": {
"messageId": createMessageId(),
"namespace": report?.event?.header?.namespace ?? "Alexa",
"name": report?.event?.header?.name ?? "ChangeReport",
"payloadVersion": "3"
},
"context": report?.context
}
"endpoint": {
"endpointId": eventSource.id,
},
payload: report?.event?.payload
},
context: report?.context
} as ChangeReport;
data = addOnline(data, eventSource);
data = addBattery(data, eventSource);
data = addPowerSensor(data, eventSource);
data = addOnline(data, eventSource);
data = addBattery(data, eventSource);
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
const accessToken = await this.getAccessToken();
data.event.endpoint.scope = {
"type": "BearerToken",
"token": accessToken,
};
// nothing to report
if (data.context === undefined && data.event.payload === undefined)
return;
data = await this.addAccessToken(data);
await this.postEvent(data);
});
await this.postEvent(data);
}
private async addAccessToken(data: any) : Promise<any> {
const accessToken = await this.getAccessToken();
if (data.event === undefined)
data.event = {};
if (data.event.endpoint === undefined)
data.event.endpoint = [];
data.event.endpoint.scope = {
"type": "BearerToken",
"token": accessToken,
};
return data;
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
return mixinDevice;
}
async releaseMixin(id: string, mixinDevice: any): Promise<void> {
const device = systemManager.getDeviceById(id);
if (device.mixins?.includes(this.id)) {
return;
}
this.console.log('release mixin', id);
this.log.a(`${device.name} was removed. The Alexa plugin will reload momentarily.`);
deviceManager.requestRestart();
}
readonly endpoints: string[] = [
'api.amazonalexa.com',
'api.eu.amazonalexa.com',
@@ -146,6 +234,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
const endpoint = await this.getAlexaEndpoint();
const self = this;
this.console.assert(!DEBUG, `event:`, data);
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
'Authorization': 'Bearer ' + accessToken,
@@ -160,25 +250,59 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
});
}
async syncDevices() {
const endpoints = await this.addOrUpdateReport();
async getEndpoints() : Promise<DiscoveryEndpoint[]> {
const endpoints: DiscoveryEndpoint[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
if (!device.mixins?.includes(this.id))
continue;
const endpoint = await this.getEndpointForDevice(device);
if (endpoint)
endpoints.push(endpoint);
}
return endpoints;
}
async onDiscoverEndpoints(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
const endpoints = await this.getEndpoints();
const data = {
"event": {
"header": {
"namespace": 'Alexa.Discovery',
"name": 'Discover.Response',
"payloadVersion": '3',
"messageId": createMessageId()
},
"payload": {
endpoints
}
}
} as Discovery;
response.send(data);
await this.saveEndpoints(endpoints);
}
async addOrUpdateReport() {
const endpoints = this.getDiscoveryEndpoints();
async syncEndpoints() {
const endpoints = await this.getEndpoints();
if (!endpoints.length)
return [];
return [];
const accessToken = await this.getAccessToken();
await this.postEvent({
const data = {
"event": {
"header": {
"namespace": "Alexa.Discovery",
"name": "AddOrUpdateReport",
"payloadVersion": "3",
"messageId": createMessageId(),
"messageId": createMessageId()
},
"payload": {
endpoints,
@@ -188,12 +312,35 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
}
}
});
};
return endpoints;
await this.postEvent(data);
await this.saveEndpoints(endpoints);
}
async deleteReport(...ids: string[]) {
async saveEndpoints(endpoints: DiscoveryEndpoint[]) {
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
const deleted = new Set(existingEndpoints);
for (const id of newEndpoints) {
deleted.delete(id);
}
const all = new Set([...existingEndpoints, ...newEndpoints]);
// save all the endpoints
this.storageSettings.values.syncedDevices = [...all];
// delete leftover endpoints
await this.deleteEndpoints(...deleted);
// prune if the delete report completed successfully
this.storageSettings.values.syncedDevices = newEndpoints;
}
async deleteEndpoints(...ids: string[]) {
if (!ids.length)
return;
@@ -219,17 +366,6 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
})
}
async canMixin(type: ScryptedDeviceType, interfaces: string[]): Promise<string[]> {
const discovery = isSupported({
type,
interfaces,
} as any);
if (!discovery)
return;
return [];
}
getAccessToken(): Promise<string> {
if (this.accessToken)
return this.accessToken;
@@ -306,9 +442,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
return this.accessToken;
}
async alexaAuthorization(request: HttpRequest, response: HttpResponse) {
const json = JSON.parse(request.body);
const { grant } = json.directive.payload;
async onAlexaAuthorization(request: HttpRequest, response: AlexaHttpResponse, directive: any) {
const { grant } = directive.payload;
this.storageSettings.values.tokenInfo = grant;
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
@@ -321,27 +456,14 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
this.storageSettings.values.apiEndpoint = undefined;
this.accessToken = undefined;
response.send(JSON.stringify({
"event": {
"header": {
"namespace": "Alexa.Authorization",
"name": "ErrorResponse",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
"type": "ACCEPT_GRANT_FAILED",
"message": `Failed to handle the AcceptGrant directive because ${reason}`
}
}
}));
response.send(authErrorResponse("ACCEPT_GRANT_FAILED", `Failed to handle the AcceptGrant directive because ${reason}`, directive));
return undefined;
});
if (accessToken !== undefined) {
try {
response.send(JSON.stringify({
response.send({
"event": {
"header": {
"namespace": "Alexa.Authorization",
@@ -351,7 +473,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
},
"payload": {}
}
}));
});
} catch (error) {
this.console.error(`AcceptGrant.Response failed because ${error}`);
@@ -363,14 +485,15 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
}
createEndpoint(device: ScryptedDevice): DiscoveryEndpoint<any> {
async getEndpointForDevice(device: ScryptedDevice) : Promise<DiscoveryEndpoint> {
if (!device)
return;
const discovery = isSupported(device);
const discovery = await supportedTypes.get(device.type)?.discover(device);
if (!discovery)
return;
const ret = Object.assign({
const data: DiscoveryEndpoint = {
endpointId: device.id,
manufacturerName: "Scrypted",
description: `${device.info?.manufacturer ?? 'Unknown'} ${device.info?.model ?? `device of type ${device.type}`}, connected via Scrypted`,
@@ -380,13 +503,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
model: device.info?.model || undefined,
serialNumber: device.info?.serialNumber || undefined,
firmwareVersion: device.info?.firmware || undefined,
//softwareVersion: device.info?.version || undefined
}
}, discovery);
softwareVersion: device.info?.version || undefined
},
displayCategories: discovery.displayCategories,
capabilities: discovery.capabilities
};
let supportedEndpointHealths: any[] = [];
if (device.interfaces.includes(ScryptedInterface.Online)) {
supportedEndpointHealths.push({
"name": "connectivity"
});
}
let supportedEndpointHealths = [{
"name": "connectivity"
}];
// {
// "name": "radioDiagnostics"
// },
@@ -400,17 +530,22 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
})
}
ret.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.2" as any,
"properties": {
"supported": supportedEndpointHealths,
"proactivelyReported": true,
"retrievable": true
if (supportedEndpointHealths.length > 0) {
data.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.EndpointHealth",
"version": "3.2",
"properties": {
"supported": supportedEndpointHealths,
"proactivelyReported": true,
"retrievable": true
}
}
},
);
}
data.capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa",
@@ -418,76 +553,20 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
}
);
//if (device.info?.mac !== undefined)
// ret.connections.push(
// {
// "type": "TCP_IP",
// "macAddress": device.info?.mac || undefined
// }
// );
return ret as any;
}
async saveEndpoints(endpoints: DiscoveryEndpoint<any>[]) {
const existingEndpoints: string[] = this.storageSettings.values.syncedDevices;
const newEndpoints = endpoints.map(endpoint => endpoint.endpointId);
const deleted = new Set(existingEndpoints);
for (const id of newEndpoints) {
deleted.delete(id);
}
const all = new Set([...existingEndpoints, ...newEndpoints]);
// save all the endpoints
this.storageSettings.values.syncedDevices = [...all];
// delete leftover endpoints
await this.deleteReport(...deleted);
// prune if the delete report completed successfully
this.storageSettings.values.syncedDevices = newEndpoints;
}
getDiscoveryEndpoints() {
const endpoints: DiscoveryEndpoint<any>[] = [];
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
if (!device.mixins?.includes(this.id))
continue;
const endpoint = this.createEndpoint(device);
if (endpoint)
endpoints.push(endpoint);
}
return endpoints;
}
async alexaDiscovery(request: HttpRequest, response: HttpResponse) {
const endpoints = this.getDiscoveryEndpoints();
const ret: DiscoverEvent<any> = {
event: {
header: {
namespace: 'Alexa.Discovery',
name: 'Discover.Response',
messageId: createMessageId(),
payloadVersion: '3',
},
payload: {
endpoints,
if (device.info?.mac !== undefined)
data.connections = [
{
"type": "TCP_IP",
"macAddress": device.info.mac
}
}
}
];
response.send(JSON.stringify(ret));
this.saveEndpoints(endpoints);
return data as any;
}
async onRequest(request: HttpRequest, response: HttpResponse) {
async onRequest(request: HttpRequest, rawResponse: HttpResponse) {
const response = new HttpResponseLoggingImpl(rawResponse, this.console);
const { authorization } = request.headers;
if (!this.validAuths.has(authorization)) {
try {
@@ -501,42 +580,81 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
catch (e) {
this.console.error(`request failed due to invalid authorization`, e);
response.send(e.message, {
code: 500
code: 500,
});
return;
}
}
try {
const body = JSON.parse(request.body);
const { directive } = body;
const { namespace } = directive.header;
const handler = this.handlers.get(namespace);
if (handler)
return handler.apply(this, arguments);
const body = JSON.parse(request.body);
const { directive } = body;
const { namespace, name } = directive.header;
const capHandler = capabilityHandlers.get(namespace);
if (capHandler) {
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
if (!device) {
response.send('Not Found', {
code: 404,
});
return;
}
this.console.assert(!DEBUG, `request: ${namespace}/${name}`);
return capHandler.apply(this, [request, response, directive, device]);
const mapName = `${namespace}/${name}`;
const handler = alexaHandlers.get(mapName);
if (handler)
return handler.apply(this, [request, response, directive]);
const deviceHandler = alexaDeviceHandlers.get(mapName);
if (deviceHandler) {
const device = systemManager.getDeviceById(directive.endpoint.endpointId);
if (!device) {
response.send(deviceErrorResponse("NO_SUCH_ENDPOINT", "The device doesn't exist in Scrypted", directive));
return;
}
response.send('Not Found', {
code: 404,
});
}
catch (e) {
response.send(e.message, {
code: 500,
});
return deviceHandler.apply(this, [request, response, directive, device]);
} else {
this.console.error(`no handler for: ${mapName}`);
}
// it is better to send a non-specific response than an error, as the API might get rate throttled
response.send(mirroredResponse(directive));
}
}
enum DeviceMixinStatus {
NotSupported = 0,
Setup = 1,
AlreadySetup = 2
}
class HttpResponseLoggingImpl implements AlexaHttpResponse {
constructor(private response: HttpResponse, private console: Console) {
}
send(body: string): void;
send(body: string, options: HttpResponseOptions): void;
send(body: Buffer): void;
send(body: Buffer, options: HttpResponseOptions): void;
send(body: any, options?: any): void {
if (!options)
options = {};
if (!options.code)
options.code = 200;
if (options.code !== 200)
this.console.error(`response error ${options.code}:`, body);
else
this.console.assert(!DEBUG, `response ${options.code}:`, body);
if (typeof body === 'object')
body = JSON.stringify(body);
this.response.send(body, options);
}
sendFile(path: string): void;
sendFile(path: string, options: HttpResponseOptions): void;
sendFile(path: any, options?: any): void {
this.response.sendFile(path, options);
}
sendSocket(socket: any, options: HttpResponseOptions): void {
this.response.sendSocket(socket, options);
}
}

View File

@@ -1,5 +0,0 @@
import {v4 as uuidv4} from 'uuid';
export function createMessageId() {
return uuidv4();
}

View File

@@ -1,190 +1,24 @@
import { HttpResponse, MotionSensor, RTCAVSignalingSetup, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, VideoCamera } from "@scrypted/sdk";
import { addSupportedType, AlexaCapabilityHandler, capabilityHandlers, EventReport, StateReport } from "./common";
import { createMessageId } from "../message";
import { Capability } from "alexa-smarthome-ts/lib/skill/Capability";
import { DisplayCategory } from "alexa-smarthome-ts";
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, Report } from "../alexa";
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
import { supportedTypes } from ".";
export function getCameraCapabilities(device: ScryptedDevice): Capability<any>[] {
const capabilities: Capability<any>[] = [
{
"type": "AlexaInterface",
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
}
} as any,
];
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
},
)
}
return capabilities;
}
addSupportedType(ScryptedDeviceType.Camera, {
probe(device) {
supportedTypes.set(ScryptedDeviceType.Camera, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel))
return;
const capabilities = getCameraCapabilities(device);
const capabilities = await getCameraCapabilities(device);
return {
displayCategories: ['CAMERA'],
capabilities
}
},
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport> {
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
};
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
return reportCameraState(device);
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor, eventDetails, eventData): Promise<EventReport> {
if (eventDetails.eventInterface !== ScryptedInterface.MotionSensor)
return undefined;
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
},
};
sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
return sendCameraEvent(eventSource, eventDetails, eventData);
}
});
export const rtcHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: HttpResponse, public directive: any) {
}
async getOptions(): Promise<RTCSignalingOptions> {
return {
proxy: true,
offer: {
type: 'offer',
sdp: this.directive.payload.offer.value,
},
disableTrickle: true,
// this could be a low resolution screen, no way of knowing, so never send a
// 1080p+ stream.
screen: {
devicePixelRatio: 1, // TODO: get this from the device
width: 1280,
height: 720,
}
}
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
return {
type: 'offer',
sdp: this.directive.payload.offer.value,
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
throw new Error("Alexa does not support trickle ICE");
}
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
this.response.send(JSON.stringify({
"event": {
"header": {
"namespace": "Alexa.RTCSessionController",
"name": "AnswerGeneratedForSession",
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
"answer": {
"format": "SDP",
"value": description.sdp,
}
}
}
}));
}
}
rtcHandlers.set('InitiateSessionWithOffer', async (request, response, directive: any,
device: ScryptedDevice & RTCSignalingChannel) => {
const session = new AlexaSignalingSession(response, directive);
const control = await device.startRTCSignalingSession(session);
control.setPlayback({
audio: true,
video: false,
})
});
capabilityHandlers.set('Alexa.RTCSessionController', async (request, response, directive: any, device: ScryptedDevice & VideoCamera) => {
const { name } = directive.header;
const handler = rtcHandlers.get(name);
if (handler)
return handler.apply(this, [request, response, directive, device]);
const { sessionId } = directive.payload;
const body = {
"event": {
"header": {
"namespace": "Alexa.RTCSessionController",
name,
"messageId": createMessageId(),
"payloadVersion": "3"
},
"payload": {
sessionId,
}
}
};
response.send(JSON.stringify(body));
});

View File

@@ -0,0 +1,194 @@
import sdk, { MediaObject, MotionSensor, ObjectDetector, ScryptedDevice, ScryptedInterface } from "@scrypted/sdk";
import { ChangeReport, DiscoveryCapability, ObjectDetectionEvent, Report, StateReport, Property } from "../../alexa";
const { mediaManager } = sdk;
export async function reportCameraState(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
data.context.properties.push({
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": classNames.map(type => ({
"imageNetClass": type
})),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
data.context.properties.push({
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
};
export async function sendCameraEvent (eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.ObjectDetector) {
// ring and motion are not valid objects
if (eventData.detections.has('ring') || eventData.detections.has('motion'))
return undefined;
console.debug('ObjectDetector event', eventData);
let mediaObj: MediaObject = undefined;
let frameImageUri: string = undefined;
try {
mediaObj = await eventSource.getDetectionInput(eventData.detectionId, eventData.eventId);
frameImageUri = await mediaManager.convertMediaObjectToUrl(mediaObj, 'image/jpeg');
} catch (e) { }
let data = {
event: {
header: {
namespace: 'Alexa.SmartVision.ObjectDetectionSensor',
name: 'ObjectDetection'
},
payload: {
"events": [eventData.detections.map(detection => {
let event = {
"eventIdentifier": eventData.eventId,
"imageNetClass": detection.className,
"timeOfSample": new Date(eventData.timestamp).toISOString(),
"uncertaintyInMilliseconds": 500
};
if (detection.id) {
event["objectIdentifier"] = detection.id;
}
if (frameImageUri) {
event["frameImageUri"] = frameImageUri;
}
return event;
})]
}
}
} as Partial<ObjectDetectionEvent>;
return data;
}
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 500
}
]
}
},
}
} as Partial<ChangeReport>;
return undefined;
};
export async function getCameraCapabilities(device: ScryptedDevice): Promise<DiscoveryCapability[]> {
const capabilities = [
{
"type": "AlexaInterface",
"interface": "Alexa.RTCSessionController",
"version": "3",
"configuration": {
isFullDuplexAudioSupported: true,
}
} as DiscoveryCapability
];
if (device.interfaces.includes(ScryptedInterface.ObjectDetector)) {
const detectionTypes = await (device as any as ObjectDetector).getObjectTypes();
const classNames = detectionTypes.classes.filter(t => t !== 'ring' && t !== 'motion').map(type => type.toLowerCase());
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.SmartVision.ObjectDetectionSensor",
"version": "1.0",
"properties": {
"supported": [{
"name": "objectDetectionClasses"
}],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"objectDetectionConfiguration": classNames.map(type => ({
"imageNetClass": type
}))
}
} as DiscoveryCapability
);
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DataController",
"instance": "Camera.SmartVisionData",
"version": "1.0",
"properties": undefined,
"configuration": {
"targetCapability": {
"name": "Alexa.SmartVision.ObjectDetectionSensor",
"version": "1.0"
},
"dataRetrievalSchema": {
"type": "JSON",
"schema": "SmartVisionData"
},
"supportedAccess": ["BY_IDENTIFIER", "BY_TIMESTAMP_RANGE"]
}
} as DiscoveryCapability
);
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
}
return capabilities;
};

View File

@@ -0,0 +1,159 @@
import { ObjectDetector, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { v4 as createMessageId } from 'uuid';
import { AlexaHttpResponse, sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { Response, WebRTCAnswerGeneratedForSessionEvent, WebRTCSessionConnectedEvent, WebRTCSessionDisconnectedEvent } from '../../alexa'
export class AlexaSignalingSession implements RTCSignalingSession {
constructor(public response: AlexaHttpResponse, public directive: any) {
}
async getOptions(): Promise<RTCSignalingOptions> {
return {
proxy: true,
offer: {
type: 'offer',
sdp: this.directive.payload.offer.value,
},
disableTrickle: true,
disableTurn: true,
// this could be a low resolution screen, no way of knowning, so never send a 1080p stream
screen: {
devicePixelRatio: 1,
width: 1280,
height: 720
}
}
}
async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise<RTCSessionDescriptionInit> {
if (type !== 'offer')
throw new Error('Alexa only supports RTC offer');
if (sendIceCandidate)
throw new Error("Alexa does not support trickle ICE");
return {
type: type,
sdp: this.directive.payload.offer.value,
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
throw new Error("Alexa does not support trickle ICE");
}
async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise<void> {
const { header, endpoint, payload } = this.directive;
const data: WebRTCAnswerGeneratedForSessionEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.name = "AnswerGeneratedForSession";
data.event.header.messageId = createMessageId();
data.event.payload.answer = {
format: 'SDP',
value: description.sdp,
};
this.response.send(data);
}
}
const sessionCache = new Map<string, RTCSessionControl>();
alexaDeviceHandlers.set('Alexa.RTCSessionController/InitiateSessionWithOffer', async (request, response, directive: any, device: ScryptedDevice & RTCSignalingChannel) => {
const { header, endpoint, payload } = directive;
const { sessionId } = payload;
const session = new AlexaSignalingSession(response, directive);
const control = await device.startRTCSignalingSession(session);
control.setPlayback({
audio: true,
video: false,
})
sessionCache.set(sessionId, control);
});
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionConnected', async (request, response, directive: any, device: ScryptedDevice) => {
const { header, endpoint, payload } = directive;
const data: WebRTCSessionConnectedEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
alexaDeviceHandlers.set('Alexa.RTCSessionController/SessionDisconnected', async (request, response, directive: any, device: ScryptedDevice) => {
const { header, endpoint, payload } = directive;
const { sessionId } = payload;
const session = sessionCache.get(sessionId);
if (session) {
sessionCache.delete(sessionId);
await session.endSession();
}
const data: WebRTCSessionDisconnectedEvent = {
"event": {
header,
endpoint,
payload
},
context: undefined
};
data.event.header.messageId = createMessageId();
response.send(data);
});
alexaDeviceHandlers.set('Alexa.SmartVision.ObjectDetectionSensor/SetObjectDetectionClasses', async (request, response, directive: any, device: ScryptedDevice & ObjectDetector) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const detectionTypes = await device.getObjectTypes();
const data: Response = {
"event": {
header,
endpoint,
payload: {}
},
"context": {
"properties": [{
"namespace": "Alexa.SmartVision.ObjectDetectionSensor",
"name": "objectDetectionClasses",
"value": detectionTypes.classes.map(type => ({
"imageNetClass": type
})),
timeOfSample: new Date().toISOString(),
uncertaintyInMilliseconds: 0
}]
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
});

View File

@@ -1,180 +0,0 @@
import { Battery, EventDetails, HttpRequest, HttpResponse, Online, PowerSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import {DiscoveryEndpoint, Directive} from 'alexa-smarthome-ts';
import { createMessageId } from "../message";
export type AlexaHandler = (request: HttpRequest, response: HttpResponse, directive: Directive) => Promise<void>
export type AlexaCapabilityHandler<T> = (request: HttpRequest, response: HttpResponse, directive: Directive, device: ScryptedDevice & T) => Promise<void>
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
export const capabilityHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export const alexaHandlers = new Map<string, AlexaCapabilityHandler<any>>();
export interface EventReport {
type: 'event';
payload?: any;
context?: any;
namespace?: string;
name?: string;
}
export interface StateReport {
type: 'state';
payload?: any;
context?: any;
namespace?: string;
name?: string;
}
export interface SupportedType {
probe(device: ScryptedDevice): Partial<DiscoveryEndpoint<any>>;
sendEvent(eventSource: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<EventReport>;
reportState(device: ScryptedDevice): Promise<StateReport>;
}
export function addSupportedType(type: ScryptedDeviceType, supportedType: SupportedType) {
supportedTypes.set(type, supportedType);
}
export function isSupported(device: ScryptedDevice) {
return supportedTypes.get(device.type)?.probe(device);
}
export function addOnline(data: any, device: ScryptedDevice & Online) : any {
if (!device.interfaces.includes(ScryptedInterface.Online))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "connectivity",
"value": {
"value": device.online ? "OK" : "UNREACHABLE",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addPowerSensor(data: any, device: ScryptedDevice & PowerSensor) : any {
if (!device.interfaces.includes(ScryptedInterface.PowerSensor))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
data.context.properties.push(
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": device.powerDetected ? "ON" : "OFF",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
export function addBattery(data: any, device: ScryptedDevice & Battery) : any {
if (!device.interfaces.includes(ScryptedInterface.Battery))
return data;
if (data.context === undefined)
data.context = {};
if (data.context.properties === undefined)
data.context.properties = [];
const lowPower = device.batteryLevel < 20;
let health = undefined;
if (lowPower) {
health = {
"state": "WARNING",
"reasons": [
"LOW_CHARGE"
]
};
}
data.context.properties.push(
{
"namespace": "Alexa.EndpointHealth",
"name": "battery",
"value": {
health,
"levelPercentage": device.batteryLevel,
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
);
return data;
}
function sendResponse(data: any, response: any, device: ScryptedDevice) {
data = addBattery(data, device);
data = addOnline(data, device);
data = addPowerSensor(data, device);
response.send(JSON.stringify(data));
}
alexaHandlers.set('ReportState', async (request, response, directive: any, device: ScryptedDevice) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint } = directive;
const report = await supportedType.reportState(device);
if (report.type === 'state') {
const data = {
"event": {
header,
endpoint,
payload: report.payload,
},
"context": report.context
};
data.event.header.name = "StateReport";
data.event.header.messageId = createMessageId();
sendResponse(data, response, device);
}
});
capabilityHandlers.set('Alexa', async (request, response, directive: any, device: ScryptedDevice) => {
const { name } = directive.header;
let handler = alexaHandlers.get(name);
if (handler)
return handler.apply(this, [request, response, directive, device]);
const { header, endpoint, payload } = directive;
const data = {
"event": {
header,
endpoint,
payload
}
};
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendResponse(data, response, device);
});

View File

@@ -1,81 +1,57 @@
import { BinarySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities } from "./camera";
import { addSupportedType, EventReport, StateReport } from "./common";
import { DisplayCategory } from "alexa-smarthome-ts";
import { MotionSensor, ObjectDetector, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities, reportCameraState, sendCameraEvent } from "./camera/capabilities";
import { DiscoveryEndpoint, DisplayCategory, Report, DoorbellPressEvent } from "../alexa";
import { supportedTypes } from ".";
addSupportedType(ScryptedDeviceType.Doorbell, {
probe(device) {
if (!device.interfaces.includes(ScryptedInterface.RTCSignalingChannel) || !device.interfaces.includes(ScryptedInterface.BinarySensor))
return;
supportedTypes.set(ScryptedDeviceType.Doorbell, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
let capabilities: any[] = [];
let category: DisplayCategory = 'DOORBELL';
const capabilities = getCameraCapabilities(device);
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
} as any,
);
return {
displayCategories: ['CAMERA'],
capabilities
if (device.interfaces.includes(ScryptedInterface.RTCSignalingChannel)) {
capabilities = await getCameraCapabilities(device);
category = 'CAMERA';
}
},
async reportState(device: ScryptedDevice & MotionSensor): Promise<StateReport>{
if (device.interfaces.includes(ScryptedInterface.BinarySensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.DoorbellEventSource",
"version": "3",
"proactivelyReported": true
} as any,
);
}
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": device.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
displayCategories: [category],
capabilities
};
},
async sendEvent(eventSource: ScryptedDevice, eventDetails, eventData): Promise<EventReport> {
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
},
};
sendReport(device: ScryptedDevice & MotionSensor & ObjectDetector): Promise<Partial<Report>>{
return reportCameraState(device);
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor & ObjectDetector, eventDetails, eventData): Promise<Partial<Report>> {
let response = await sendCameraEvent(eventSource, eventDetails, eventData);
if (response)
return response;
if (eventDetails.eventInterface === ScryptedInterface.BinarySensor && eventData === true)
return {
type: 'event',
namespace: 'Alexa.DoorbellEventSource',
name: 'DoorbellPress',
payload: {
"cause": {
"type": "PHYSICAL_INTERACTION"
event: {
header: {
namespace: 'Alexa.DoorbellEventSource',
name: 'DoorbellPress'
},
"timestamp": new Date().toISOString(),
}
};
payload: {
"cause": {
"type": "PHYSICAL_INTERACTION"
},
"timestamp": new Date(eventDetails.eventTime).toISOString(),
}
}
} as Partial<DoorbellPressEvent>;
}
});

View File

@@ -1,14 +1,13 @@
import { BinarySensor, Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { getCameraCapabilities } from "./camera";
import { addSupportedType, EventReport, StateReport } from "./common";
import { DisplayCategory } from "alexa-smarthome-ts";
import { Entry, EntrySensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report } from "../alexa";
import { supportedTypes } from ".";
addSupportedType(ScryptedDeviceType.Garage, {
probe(device) {
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
supportedTypes.set(ScryptedDeviceType.Garage, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
if (!device.interfaces.includes(ScryptedInterface.EntrySensor))
return;
const capabilities = getCameraCapabilities(device);
const capabilities: DiscoveryCapability[] = [];
capabilities.push(
{
"type": "AlexaInterface",
@@ -115,19 +114,16 @@ addSupportedType(ScryptedDeviceType.Garage, {
}
]
}
} as any,
},
);
return {
displayCategories: ['GARAGE_DOOR'] as any,
displayCategories: ['GARAGE_DOOR'],
capabilities
}
},
async reportState(eventSource: ScryptedDevice & EntrySensor): Promise<StateReport> {
async sendReport(eventSource: ScryptedDevice & EntrySensor): Promise<Partial<Report>> {
return {
type: 'state',
namespace: 'Alexa',
name: 'StateReport',
context: {
"properties": [
{
@@ -142,14 +138,12 @@ addSupportedType(ScryptedDeviceType.Garage, {
}
};
},
async sendEvent(eventSource: ScryptedDevice & EntrySensor, eventDetails, eventData): Promise<EventReport> {
async sendEvent(eventSource: ScryptedDevice & Entry & EntrySensor, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface !== ScryptedInterface.EntrySensor)
return undefined;
return {
type: 'event',
namespace: 'Alexa',
name: 'ChangeReport',
event: {
payload: {
change: {
cause: {
@@ -167,6 +161,30 @@ addSupportedType(ScryptedDeviceType.Garage, {
]
}
},
};
}
}
} as Partial<ChangeReport>;
},
async setState(eventSource: ScryptedDevice & Entry & EntrySensor, payload: any): Promise<Partial<Report>> {
if (payload.mode === 'Position.Up') {
await eventSource.openEntry();
}
else if (payload.mode === 'Position.Down') {
await eventSource.closeEntry();
}
return {
context: {
"properties": [
{
"namespace": "Alexa.ModeController",
"instance": "GarageDoor.Position",
"name": "mode",
"value": payload.mode,
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
}
]
}
};
}
});

View File

@@ -0,0 +1,31 @@
import { ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { sendDeviceResponse } from "../../common";
import { alexaDeviceHandlers } from "../../handlers";
import { v4 as createMessageId } from 'uuid';
import { Response } from "../../alexa";
async function sendResponse (request, response, directive: any, device: ScryptedDevice) {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
const report = await supportedType.setState(device, payload);
const data = {
"event": {
header,
endpoint,
payload
},
context: report?.context
} as Response;
data.event.header.name = "Response";
data.event.header.messageId = createMessageId();
sendDeviceResponse(data, response, device);
}
alexaDeviceHandlers.set('Alexa.ModeController/SetMode', sendResponse);
alexaDeviceHandlers.set('Alexa.ModeController/AdjustMode', sendResponse);

View File

@@ -1,6 +1,21 @@
import { ScryptedDeviceType, ScryptedDevice, EventDetails } from '@scrypted/sdk';
import { DiscoveryEndpoint, Report } from '../alexa';
export interface SupportedType {
discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>>;
sendEvent(device: ScryptedDevice, eventDetails: EventDetails, eventData: any): Promise<Partial<Report>>;
sendReport(device: ScryptedDevice): Promise<Partial<Report>>;
setState?(device: ScryptedDevice, payload: any): Promise<Partial<Report>>;
}
export const supportedTypes = new Map<ScryptedDeviceType, SupportedType>();
import '../handlers';
import './camera';
import './camera/handlers';
import './doorbell';
import './garagedoor';
export { isSupported} from './common';
import './switch';
import './switch/handlers';
import './sensor';
import './securitysystem';

View File

@@ -0,0 +1,179 @@
import { EventDetails, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SecuritySystem, SecuritySystemMode } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
import { supportedTypes } from ".";
function getArmState(mode: SecuritySystemMode): string {
switch(mode) {
case SecuritySystemMode.AwayArmed:
return 'ARMED_AWAY';
case SecuritySystemMode.HomeArmed:
return 'ARMED_STAY';
case SecuritySystemMode.NightArmed:
return 'ARMED_NIGHT';
case SecuritySystemMode.Disarmed:
return 'DISARMED';
}
}
supportedTypes.set(ScryptedDeviceType.SecuritySystem, {
async discover(device: ScryptedDevice & SecuritySystem): Promise<Partial<DiscoveryEndpoint>> {
const capabilities: DiscoveryCapability[] = [];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.SecuritySystem)) {
const supportedModes = device.securitySystemState.supportedModes;
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.SecurityPanelController",
"version": "3",
"properties": {
"supported": [
{
"name": "armState"
},
{
"name": "burglaryAlarm"
},
//{
// "name": "waterAlarm"
//},
//{
// "name": "fireAlarm"
//},
//{
// "name": "carbonMonoxideAlarm"
//}
],
"proactivelyReported": true,
"retrievable": true
},
"configuration": {
"supportedArmStates": supportedModes.map(mode => {
return {
"value": getArmState(mode)
}
}),
"supportedAuthorizationTypes": [
{
"type": "FOUR_DIGIT_PIN"
}
]
}
} as DiscoveryCapability
);
displayCategories.push('SECURITY_PANEL');
}
if (capabilities.length === 0)
return;
return {
displayCategories,
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & SecuritySystem): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.SecuritySystem)) {
data.context.properties.push({
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventSource.securitySystemState.mode),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property);
data.context.properties.push({
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property);
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & SecuritySystem, eventDetails: EventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "mode") {
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventData),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
},
context: {
properties: [{
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventSource.securitySystemState.triggered ? "ALARM" : "OK",
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property]
}
} as Partial<ChangeReport>;
}
if (eventDetails.eventInterface === ScryptedInterface.SecuritySystem && eventDetails.property === "triggered") {
return {
event: {
payload: {
change: {
cause: {
type: "RULE_TRIGGER"
},
properties: [
{
"namespace": "Alexa.SecurityPanelController",
"name": "burglaryAlarm",
"value": {
"value": eventData ? "ALARM" : "OK"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
},
context: {
properties: [{
"namespace": "Alexa.SecurityPanelController",
"name": "armState",
"value": getArmState(eventSource.securitySystemState.mode),
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
} as Property]
}
} as Partial<ChangeReport>;
}
return undefined;
}
});

View File

@@ -0,0 +1,196 @@
import { EntrySensor, MotionSensor, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, Thermometer } from "@scrypted/sdk";
import { DiscoveryEndpoint, DiscoveryCapability, ChangeReport, Report, StateReport, DisplayCategory, ChangePayload, Property } from "../alexa";
import { supportedTypes } from ".";
supportedTypes.set(ScryptedDeviceType.Sensor, {
async discover(device: ScryptedDevice): Promise<Partial<DiscoveryEndpoint>> {
const capabilities: DiscoveryCapability[] = [];
const displayCategories: DisplayCategory[] = [];
if (device.interfaces.includes(ScryptedInterface.Thermometer)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.TemperatureSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "temperature"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('TEMPERATURE_SENSOR');
}
if (device.interfaces.includes(ScryptedInterface.EntrySensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.ContactSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('CONTACT_SENSOR');
}
if (device.interfaces.includes(ScryptedInterface.MotionSensor)) {
capabilities.push(
{
"type": "AlexaInterface",
"interface": "Alexa.MotionSensor",
"version": "3",
"properties": {
"supported": [
{
"name": "detectionState"
}
],
"proactivelyReported": true,
"retrievable": true
}
} as DiscoveryCapability
);
displayCategories.push('MOTION_SENSOR');
}
if (capabilities.length === 0)
return;
return {
displayCategories: displayCategories,
capabilities
}
},
async sendReport(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer): Promise<Partial<Report>> {
let data = {
context: {
properties: []
}
} as Partial<StateReport>;
if (eventSource.interfaces.includes(ScryptedInterface.Thermometer)) {
data.context.properties.push({
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": eventSource.temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.EntrySensor)) {
data.context.properties.push({
"namespace": "Alexa.ContactSensor",
"name": "detectionState",
"value": eventSource.entryOpen ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
if (eventSource.interfaces.includes(ScryptedInterface.MotionSensor)) {
data.context.properties.push({
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventSource.motionDetected ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date().toISOString(),
"uncertaintyInMilliseconds": 0
});
}
return data;
},
async sendEvent(eventSource: ScryptedDevice & MotionSensor & EntrySensor & Thermometer, eventDetails, eventData): Promise<Partial<Report>> {
if (eventDetails.eventInterface === ScryptedInterface.MotionSensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.MotionSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface === ScryptedInterface.EntrySensor)
return {
event: {
payload: {
change: {
cause: {
type: "PHYSICAL_INTERACTION"
},
properties: [
{
"namespace": "Alexa.ContactSensor",
"name": "detectionState",
"value": eventData ? "DETECTED" : "NOT_DETECTED",
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
if (eventDetails.eventInterface === ScryptedInterface.Thermometer)
return {
event: {
payload: {
change: {
cause: {
type: "PERIODIC_POLL"
},
properties: [
{
"namespace": "Alexa.TemperatureSensor",
"name": "temperature",
"value": {
"value": eventSource.temperature,
"scale": "CELSIUS"
},
"timeOfSample": new Date(eventDetails.eventTime).toISOString(),
"uncertaintyInMilliseconds": 0
} as Property
]
}
} as ChangePayload,
}
} as Partial<ChangeReport>;
return undefined;
}
});

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.Switch, {
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: ['SWITCH'],
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

@@ -0,0 +1,55 @@
import { OnOff, ScryptedDevice } from "@scrypted/sdk";
import { supportedTypes } from "..";
import { sendDeviceResponse } from "../../common";
import { v4 as createMessageId } from 'uuid';
import { alexaDeviceHandlers } from "../../handlers";
import { Directive, Response } from "../../alexa";
function commonResponse(header, endpoint, payload, response, device: ScryptedDevice & OnOff) {
const data : Response = {
"event": {
header,
endpoint,
payload
},
"context": {
"properties": [
{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": device.on ? "ON" : "OFF",
"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.PowerController/TurnOn', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.turnOn();
commonResponse(header, endpoint, payload, response, device);
});
alexaDeviceHandlers.set('Alexa.PowerController/TurnOff', async (request, response, directive: Directive, device: ScryptedDevice & OnOff) => {
const supportedType = supportedTypes.get(device.type);
if (!supportedType)
return;
const { header, endpoint, payload } = directive;
await device.turnOff();
commonResponse(header, endpoint, payload, response, device);
});

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.115",
"version": "0.0.119",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.115",
"version": "0.0.119",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.115",
"version": "0.0.119",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",

View File

@@ -2,6 +2,7 @@ import AxiosDigestAuth from '@koush/axios-digest-auth';
import { Readable } from 'stream';
import https from 'https';
import { IncomingMessage } from 'http';
import { amcrestHttpsAgent, getDeviceInfo } from './probe';
export enum AmcrestEvent {
MotionStart = "Code=VideoMotion;action=Start",
@@ -17,12 +18,10 @@ export enum AmcrestEvent {
PhoneCallDetectStop = "Code=PhoneCallDetect;action=Stop",
DahuaTalkInvite = "Code=CallNoAnswered;action=Start",
DahuaTalkHangup = "Code=PassiveHungup;action=Start",
DahuaCallDeny = "Code=HungupPhone;action=Pulse",
DahuaTalkPulse = "Code=_CallNoAnswer_;action=Pulse",
}
export const amcrestHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
export class AmcrestCameraClient {
digestAuth: AxiosDigestAuth;
@@ -34,6 +33,16 @@ export class AmcrestCameraClient {
});
}
async checkTwoWayAudio() {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/cgi-bin/devAudioOutput.cgi?action=getCollect`,
});
return (response.data as string).includes('result=1');
}
// appAutoStart=true
// deviceType=IP4M-1041B
// hardwareVersion=1.00
@@ -42,30 +51,7 @@ export class AmcrestCameraClient {
// updateSerial=IPC-AW46WN-S2
// updateSerialCloudUpgrade=IPC-AW46WN-.....
async getDeviceInfo() {
const response = await this.digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
});
const lines = (response.data as string).split('\n');
const vals: {
[key: string]: string,
} = {};
for (const line of lines) {
let index = line.indexOf('=');
if (index === -1)
index = line.length;
const k = line.substring(0, index);
const v = line.substring(index + 1);
vals[k] = v.trim();
}
return {
deviceType: vals.deviceType,
hardwareVersion: vals.hardwareVersion,
serialNumber: vals.serialNumber,
}
return getDeviceInfo(this.digestAuth, this.ip);
}
async jpegSnapshot(): Promise<Buffer> {

View File

@@ -5,7 +5,8 @@ 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, amcrestHttpsAgent } from "./amcrest-api";
import { AmcrestCameraClient, AmcrestEvent } from "./amcrest-api";
import { amcrestHttpsAgent } from './probe';
const { mediaManager } = sdk;
@@ -37,19 +38,6 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
this.updateDeviceInfo();
this.updateManagementUrl();
}
updateManagementUrl() {
const ip = this.storage.getItem('ip');
if (!ip)
return;
const info = this.info || {};
const managementUrl = `http://${ip}`;
if (info.managementUrl !== managementUrl) {
info.managementUrl = managementUrl;
this.info = info;
}
}
getRecordingStreamCurrentTime(recordingStream: MediaObject): Promise<number> {
@@ -80,9 +68,16 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
}
async updateDeviceInfo(): Promise<void> {
if (this.info)
const ip = this.storage.getItem('ip');
if (!ip)
return;
const deviceInfo = {};
const managementUrl = `http://${ip}`;
const deviceInfo: DeviceInformation = {
...this.info,
ip,
managementUrl,
};
const deviceParameters = [
{ action: "getVendor", replace: "vendor=", parameter: "manufacturer" },
@@ -161,6 +156,8 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
const client = new AmcrestCameraClient(this.getHttpAddress(), this.getUsername(), this.getPassword(), this.console);
const events = await client.listenEvents();
const doorbellType = this.storage.getItem('doorbellType');
const callerId = this.storage.getItem('callerID');
const multipleCallIds = this.storage.getItem('multipleCallIds') === 'true';
let pulseTimeout: NodeJS.Timeout;
@@ -187,11 +184,21 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
|| event === AmcrestEvent.PhoneCallDetectStart
|| event === AmcrestEvent.AlarmIPCStart
|| event === AmcrestEvent.DahuaTalkInvite) {
this.binaryState = true;
if (event === AmcrestEvent.DahuaTalkInvite && payload && multipleCallIds)
{
if (payload.includes(callerId))
{
this.binaryState = true;
}
} else
{
this.binaryState = true;
}
}
else if (event === AmcrestEvent.TalkHangup
|| event === AmcrestEvent.PhoneCallDetectStop
|| event === AmcrestEvent.AlarmIPCStop
|| event === AmcrestEvent.DahuaCallDeny
|| event === AmcrestEvent.DahuaTalkHangup) {
this.binaryState = false;
}
@@ -246,6 +253,36 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
if (!twoWayAudio)
twoWayAudio = isDoorbell ? 'Amcrest' : 'None';
if (doorbellType == DAHUA_DOORBELL_TYPE)
{
ret.push(
{
title: 'Multiple Call Buttons',
key: 'multipleCallIds',
description: 'Some Dahua Doorbells integrate multiple Call Buttons for apartment buildings.',
type: 'boolean',
value: (this.storage.getItem('multipleCallIds') === 'true').toString(),
}
);
}
const multipleCallIds = this.storage.getItem('multipleCallIds');
if (multipleCallIds)
{
ret.push(
{
title: 'Caller ID',
key: 'callerID',
description: 'Caller ID',
type: 'number',
value: this.storage.getItem('callerID'),
}
)
}
ret.push(
{
@@ -266,7 +303,11 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
);
return ret;
}
async takeSmartCameraPicture(option?: PictureOptions): Promise<MediaObject> {
return this.createMediaObject(await this.getClient().jpegSnapshot(), 'image/jpeg');
@@ -441,7 +482,7 @@ class AmcrestCamera extends RtspSmartCamera implements VideoCameraConfiguration,
interfaces.push(ScryptedInterface.VideoRecorder);
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
this.updateManagementUrl();
this.updateDeviceInfo();
}
async startIntercom(media: MediaObject): Promise<void> {
@@ -550,9 +591,10 @@ class AmcrestProvider extends RtspProvider {
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
let twoWayAudio: string;
if (!skipValidate) {
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
try {
const api = new AmcrestCameraClient(httpAddress, username, password, this.console);
const deviceInfo = await api.getDeviceInfo();
settings.newCamera = deviceInfo.deviceType;
@@ -563,6 +605,16 @@ class AmcrestProvider extends RtspProvider {
this.console.error('Error adding Amcrest camera', e);
throw e;
}
try {
if (await api.checkTwoWayAudio()) {
// onvif seems to work better than Amcrest, except for AD110.
twoWayAudio = 'ONVIF';
}
}
catch (e) {
this.console.warn('Error probing two way audio', e);
}
}
settings.newCamera ||= 'Hikvision Camera';
@@ -574,6 +626,8 @@ class AmcrestProvider extends RtspProvider {
device.putSetting('password', password);
device.setIPAddress(settings.ip?.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
return nativeId;
}

View File

@@ -0,0 +1,42 @@
import https from 'https';
export const amcrestHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
// appAutoStart=true
// deviceType=IP4M-1041B
// hardwareVersion=1.00
// processor=SSC327DE
// serialNumber=12345
// updateSerial=IPC-AW46WN-S2
import AxiosDigestAuth from "@koush/axios-digest-auth";
// updateSerialCloudUpgrade=IPC-AW46WN-.....
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
const response = await digestAuth.request({
httpsAgent: amcrestHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${address}/cgi-bin/magicBox.cgi?action=getSystemInfo`,
});
const lines = (response.data as string).split('\n');
const vals: {
[key: string]: string,
} = {};
for (const line of lines) {
let index = line.indexOf('=');
if (index === -1)
index = line.length;
const k = line.substring(0, index);
const v = line.substring(index + 1);
vals[k] = v.trim();
}
return {
deviceType: vals.deviceType,
hardwareVersion: vals.hardwareVersion,
serialNumber: vals.serialNumber,
}
}

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.63",
"version": "0.2.78",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/arlo",
"version": "0.6.5",
"version": "0.6.7",
"description": "Arlo Plugin for Scrypted",
"keywords": [
"scrypted",

View File

@@ -27,7 +27,7 @@ from .request import Request
from .mqtt_stream_async import MQTTStream
from .sse_stream_async import EventStream
from .logging import logger
# Import all of the other stuff.
from datetime import datetime
@@ -227,7 +227,7 @@ class Arlo(object):
when subsequent calls to /notify are made.
"""
async def heartbeat(self, basestations, interval=30):
while self.event_stream and self.event_stream.connected:
while self.event_stream and self.event_stream.active:
for basestation in basestations:
try:
self.Ping(basestation)
@@ -378,7 +378,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'motionDetected')], callbackwrapper)
)
def SubscribeToBatteryEvents(self, basestation, camera, callback):
"""
@@ -403,7 +405,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'batteryLevel')], callbackwrapper)
)
def SubscribeToDoorbellEvents(self, basestation, doorbell, callback):
"""
@@ -437,7 +441,9 @@ class Arlo(object):
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['is'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'buttonPressed')], callbackwrapper)
)
def SubscribeToSDPAnswers(self, basestation, camera, callback):
"""
@@ -456,14 +462,16 @@ class Arlo(object):
def callbackwrapper(self, event):
properties = event.get("properties", {})
stop = None
stop = None
if properties.get("type") == "answerSdp":
stop = callback(properties.get("data"))
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
)
def SubscribeToCandidateAnswers(self, basestation, camera, callback):
"""
@@ -482,14 +490,16 @@ class Arlo(object):
def callbackwrapper(self, event):
properties = event.get("properties", {})
stop = None
stop = None
if properties.get("type") == "answerCandidate":
stop = callback(properties.get("data"))
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper))
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, ['pushToTalk'], callbackwrapper)
)
async def HandleEvents(self, basestation, resource, actions, callback):
"""
@@ -502,9 +512,17 @@ class Arlo(object):
await self.Subscribe()
async def loop_action_listener(action):
# in this function, action can either be a tuple or a string
# if it is a tuple, we expect there to be a property key in the tuple
property = None
if isinstance(action, tuple):
action, property = action
if not isinstance(action, str):
raise Exception('Actions must be either a tuple or a str')
seen_events = {}
while self.event_stream.active:
event, _ = await self.event_stream.get(resource, [action], seen_events)
event, _ = await self.event_stream.get(resource, action, property, seen_events)
if event is None or self.event_stream is None \
or self.event_stream.event_stream_stop_event.is_set():
@@ -514,7 +532,7 @@ class Arlo(object):
response = callback(self, event.item)
# always requeue so other listeners can see the event too
self.event_stream.requeue(event, resource, action)
self.event_stream.requeue(event, resource, action, property)
if response is not None:
return response
@@ -606,7 +624,13 @@ class Arlo(object):
return nl.stream_url_dict['url'].replace("rtsp://", "rtsps://")
return None
return await self.TriggerAndHandleEvent(basestation, resource, ["is"], trigger, callback)
return await self.TriggerAndHandleEvent(
basestation,
resource,
[("is", "activityState")],
trigger,
callback,
)
def StartPushToTalk(self, basestation, camera):
url = f'https://{self.BASE_URL}/hmsweb/users/devices/{self.user_id}_{camera.get("deviceId")}/pushtotalk'
@@ -644,8 +668,6 @@ class Arlo(object):
async def TriggerFullFrameSnapshot(self, basestation, camera):
"""
This function causes the camera to record a fullframe snapshot.
The presignedFullFrameSnapshotUrl url is returned.
Use DownloadSnapshot() to download the actual image file.
"""
resource = f"cameras/{camera.get('deviceId')}"
@@ -676,4 +698,14 @@ class Arlo(object):
return url
return None
return await self.TriggerAndHandleEvent(basestation, resource, ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"], trigger, callback)
return await self.TriggerAndHandleEvent(
basestation,
resource,
[
(action, property)
for action in ["fullFrameSnapshotAvailable", "lastImageSnapshotAvailable", "is"]
for property in ["presignedFullFrameSnapshotUrl", "presignedLastImageUrl"]
],
trigger,
callback,
)

View File

@@ -28,7 +28,7 @@ from .logging import logger
class Stream:
"""This class provides a queue-based EventStream object."""
def __init__(self, arlo, expire=10):
def __init__(self, arlo, expire=5):
self.event_stream = None
self.initializing = True
self.connected = False
@@ -43,7 +43,7 @@ class Stream:
self.event_loop = asyncio.get_event_loop()
self.event_loop.create_task(self._clean_queues())
self.event_loop.create_task(self._refresh_interval())
def __del__(self):
self.disconnect()
@@ -83,11 +83,16 @@ class Stream:
self.refresh_loop_signal.put_nowait(object())
async def _clean_queues(self):
interval = self.expire * 2
interval = self.expire * 4
await asyncio.sleep(interval)
while not self.event_stream_stop_event.is_set():
for key, q in self.queues.items():
# since we interrupt the cleanup loop after every queue, there's
# a chance the self.queues dict is modified during iteration.
# so, we first make a copy of all the items of the dict and any
# new queues will be processed on the next loop through
queue_items = [i for i in self.queues.items()]
for key, q in queue_items:
if q.empty():
continue
@@ -114,81 +119,47 @@ class Stream:
if num_dropped > 0:
logger.debug(f"Cleaned {num_dropped} events from queue {key}")
# cleanup is not urgent, so give other tasks a chance
await asyncio.sleep(0.1)
await asyncio.sleep(interval)
async def get(self, resource, actions, skip_uuids={}):
if len(actions) == 1:
action = actions[0]
async def get(self, resource, action, property=None, skip_uuids={}):
if not property:
key = f"{resource}/{action}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
first_requeued = None
while True:
event = await q.get()
q.task_done()
if not event:
# exit signal received
return None, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so sleep and give the next
# subscriber a chance
q.put_nowait(event)
await asyncio.sleep(random.uniform(0, 0.01))
continue
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
else:
while True:
for action in actions:
key = f"{resource}/{action}"
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
if q.empty():
continue
first_requeued = None
while True:
event = await q.get()
q.task_done()
first_requeued = None
while not q.empty():
event = q.get_nowait()
q.task_done()
if not event:
# exit signal received
return None, action
if not event:
# exit signal received
return None, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so go to the next queue
q.put_nowait(event)
break
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
if first_requeued is not None and first_requeued is event:
# if we reach here, we've cycled through the whole queue
# and found nothing for us, so sleep and give the next
# subscriber a chance
q.put_nowait(event)
await asyncio.sleep(random.uniform(0, 0.01))
continue
if event.expired:
continue
elif event.uuid in skip_uuids:
q.put_nowait(event)
if first_requeued is None:
first_requeued = event
else:
return event, action
async def start(self):
raise NotImplementedError()
@@ -203,15 +174,31 @@ class Stream:
resource = response.get('resource')
action = response.get('action')
key = f"{resource}/{action}"
now = time.time()
event = StreamEvent(response, now, now + self.expire)
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
now = time.time()
q.put_nowait(StreamEvent(response, now, now + self.expire))
q.put_nowait(event)
def requeue(self, event, resource, action):
key = f"{resource}/{action}"
# for optimized lookups, notify listeners of individual properties
properties = response.get('properties', {})
for property in properties.keys():
key = f"{resource}/{action}/{property}"
if key not in self.queues:
q = self.queues[key] = asyncio.Queue()
else:
q = self.queues[key]
q.put_nowait(event)
def requeue(self, event, resource, action, property=None):
if not property:
key = f"{resource}/{action}"
else:
key = f"{resource}/{action}/{property}"
self.queues[key].put_nowait(event)
def disconnect(self):

View File

@@ -1,26 +1,28 @@
{
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.13",
"dependencies": {
"@eneris/push-receiver": "../../external/push-receiver",
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
}
@@ -40,39 +42,9 @@
"@types/node": "^16.9.0"
}
},
"../../external/push-receiver": {
"name": "@eneris/push-receiver",
"version": "3.0.2",
"license": "MIT",
"dependencies": {
"axios": "^0.27.1",
"http_ece": "^1.0.5",
"long": "^5.2.0",
"protobufjs": "^6.11.2",
"request-promise": "^4.2.6"
},
"devDependencies": {
"@types/jest": "^28.1.0",
"@types/long": "^4.0.1",
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.4.6",
"http-proxy": "^1.16.2",
"husky": "^7.0.4",
"jest": "^28.0.2",
"ts-jest": "^28.0.4",
"typescript": "^4.4.3",
"yargs": "^17.2.1"
},
"engines": {
"node": ">=16"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.56",
"version": "0.2.82",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -108,8 +80,82 @@
}
},
"node_modules/@eneris/push-receiver": {
"resolved": "../../external/push-receiver",
"link": true
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
"dependencies": {
"axios": "^1.2.1",
"http_ece": "^1.0.5",
"long": "^5.2.1",
"protobufjs": "^7.1.2"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@eneris/push-receiver/node_modules/axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@scrypted/common": {
"resolved": "../../common",
@@ -137,6 +183,12 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -155,8 +207,7 @@
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"node_modules/ansi-regex": {
"version": "1.1.1",
@@ -730,6 +781,17 @@
"he": "bin/he"
}
},
"node_modules/http_ece": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
"dependencies": {
"urlsafe-base64": "~1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
@@ -1160,6 +1222,11 @@
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
"dev": true
},
"node_modules/long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -1432,6 +1499,29 @@
"node": ">= 0.6.6"
}
},
"node_modules/protobufjs": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -1636,6 +1726,11 @@
"node": ">=0.8.0"
}
},
"node_modules/urlsafe-base64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
},
"node_modules/utile": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
@@ -1889,28 +1984,82 @@
},
"dependencies": {
"@eneris/push-receiver": {
"version": "file:../../external/push-receiver",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
"requires": {
"@types/jest": "^28.1.0",
"@types/long": "^4.0.1",
"@types/node": "^17.0.29",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"axios": "^0.27.1",
"eslint": "^8.14.0",
"eslint-plugin-jest": "^26.4.6",
"axios": "^1.2.1",
"http_ece": "^1.0.5",
"http-proxy": "^1.16.2",
"husky": "^7.0.4",
"jest": "^28.0.2",
"long": "^5.2.0",
"protobufjs": "^6.11.2",
"request-promise": "^4.2.6",
"ts-jest": "^28.0.4",
"typescript": "^4.4.3",
"yargs": "^17.2.1"
"long": "^5.2.1",
"protobufjs": "^7.1.2"
},
"dependencies": {
"axios": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
}
}
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"requires": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@scrypted/common": {
"version": "file:../../common",
"requires": {
@@ -1964,6 +2113,12 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
@@ -1982,8 +2137,7 @@
"@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
"dev": true
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"ansi-regex": {
"version": "1.1.1",
@@ -2399,6 +2553,14 @@
"integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
"dev": true
},
"http_ece": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
"requires": {
"urlsafe-base64": "~1.0.0"
}
},
"http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
@@ -2737,6 +2899,11 @@
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
"dev": true
},
"long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -2966,6 +3133,25 @@
"winston": "0.8.x"
}
},
"protobufjs": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -3109,6 +3295,11 @@
"integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==",
"dev": true
},
"urlsafe-base64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
},
"utile": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",

View File

@@ -37,21 +37,23 @@
]
},
"dependencies": {
"@eneris/push-receiver": "^3.1.4",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@eneris/push-receiver": "../../external/push-receiver",
"axios": "^0.25.0",
"bpmux": "^8.1.3",
"debug": "^4.3.1",
"http-proxy": "^1.18.1",
"lodash": "^4.17.21",
"nat-upnp": "file:./node-nat-upnp",
"query-string": "^6.14.1"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/http-proxy": "^1.17.5",
"@types/lodash": "^4.14.191",
"@types/nat-upnp": "^1.1.2",
"@types/node": "^18.11.18"
},
"version": "0.1.11"
"version": "0.1.13"
}

View File

@@ -1,23 +1,23 @@
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
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 crypto from 'crypto';
import { once } from 'events';
import http from 'http';
import https from 'https';
import HttpProxy from 'http-proxy';
import https from 'https';
import throttle from "lodash/throttle";
import upnp from 'nat-upnp';
import net from 'net';
import net, { AddressInfo } from 'net';
import os from 'os';
import path from 'path';
import qs from 'query-string';
import { Duplex } from 'stream';
import tls from 'tls';
import Url from 'url';
import type { CORSControlLegacy } from '../../../server/src/services/cors';
import { createSelfSignedCertificate } from '../../../server/src/cert';
import { PushManager } from './push';
import tls from 'tls';
const { deviceManager, endpointManager, systemManager } = sdk;
@@ -547,6 +547,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
};
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
this.console.log(req.socket?.remoteAddress, req.url);
const url = Url.parse(req.url);
if (url.path.startsWith('/web/oauth/callback') && url.query) {
const query = qs.parse(url.query);
@@ -620,7 +622,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
});
this.proxy.on('error', () => { });
this.proxy.on('proxyRes', (res, req) => {
res.headers['X-Scrypted-Cloud'] = 'true';
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
});

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/core",
"version": "0.1.91",
"version": "0.1.103",
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
"author": "Scrypted",
"license": "Apache-2.0",

View File

@@ -47,12 +47,11 @@ export class Scheduler {
throw new Error('sunrise/sunset clock not supported');
}
const ret: ScryptedDevice = {
async setName() { },
async setType() { },
async setRoom() { },
async setMixins() { },
async probe() { return true },
listen(event: EventListenerOptions, callback, source?: ScryptedDeviceBase) {
function reschedule(): Date {

View File

@@ -1,8 +1,7 @@
import { BufferConverter, BufferConvertorOptions, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import sdk from "@scrypted/sdk";
import sdk, { BufferConverter, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseOptions, MediaObject, RequestMediaObject, ScryptedDeviceBase, ScryptedMimeTypes } from "@scrypted/sdk";
import crypto from 'crypto';
import mime from "mime/lite";
import path from 'path';
import crypto from 'crypto';
const { endpointManager } = sdk;

View File

@@ -1,3 +1,4 @@
import { 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 { StorageSettings } from "@scrypted/sdk/storage-settings";
import fs from 'fs';
@@ -237,3 +238,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
}
export default ScryptedCore;
export async function fork() {
return {
tsCompile,
}
}

View File

@@ -3,10 +3,13 @@ import { scryptedEval } from "./scrypted-eval";
import { monacoEvalDefaults } from "./monaco";
import { createScriptDevice, ScriptDeviceImpl } from "@scrypted/common/src/eval/scrypted-eval";
import { ScriptCoreNativeId } from "./script-core";
import { PluginAPIProxy } from "../../../server/src/plugin/plugin-api";
const { log, deviceManager, systemManager } = sdk;
export class Script extends ScryptedDeviceBase implements Scriptable, Program, ScriptDeviceImpl {
apiProxy: PluginAPIProxy;
constructor(nativeId: string) {
super(nativeId);
}
@@ -67,6 +70,8 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
}
prepareScript() {
this.apiProxy?.removeListeners();
Object.assign(this, createScriptDevice([
ScryptedInterface.Scriptable,
ScryptedInterface.Program,
@@ -79,10 +84,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
try {
const data = JSON.parse(this.storage.getItem('data'));
const { value, defaultExport } = await scryptedEval(this, data['script.ts'], Object.assign({
const { value, defaultExport, apiProxy } = await scryptedEval(this, data['script.ts'], Object.assign({
device: this,
}, variables));
this.apiProxy = apiProxy;
await this.postRunScript(defaultExport);
return value;
}
@@ -95,10 +102,12 @@ export class Script extends ScryptedDeviceBase implements Scriptable, Program, S
async eval(source: ScriptSource, variables?: { [name: string]: any }) {
this.prepareScript();
const { value, defaultExport } = await scryptedEval(this, source.script, Object.assign({
const { value, defaultExport, apiProxy } = await scryptedEval(this, source.script, Object.assign({
device: this,
}, variables));
this.apiProxy = apiProxy;
await this.postRunScript(defaultExport);
return value;
}

View File

@@ -3,14 +3,20 @@ import { addAccessControlsForInterface } from "@scrypted/sdk/acl";
import { StorageSettings } from "@scrypted/sdk/storage-settings";
export const UsersNativeId = 'users';
type DBUser = { username: string, aclId: string };
type DBUser = { username: string, admin: boolean };
export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
storageSettings = new StorageSettings(this, {
devices: {
title: 'Devices',
description: 'The devices this user can access. Admin users can access all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
type: 'device',
defaultAccess: {
title: 'Default Access',
description: 'Grant access to @scrypted/core and @scrypted/webrtc',
defaultValue: true,
type: 'boolean',
},
interfaces: {
title: 'Interfaces',
description: 'The interfaces this user can access. Admin users can access all interfaces on all devices. Scrypted NVR users should use NVR Permissions to grant access to the NVR and associated cameras.',
type: 'interface',
multiple: true,
defaultValue: [],
},
@@ -19,26 +25,27 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
const self = sdk.deviceManager.getDeviceState(this.nativeId);
const ret: ScryptedUserAccessControl = {
const ret: ScryptedUserAccessControl = {
devicesAccessControls: [
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
...this.storageSettings.values.devices.map((id: string) => ({
id,
})),
...this.storageSettings.values.defaultAccess
? [
// grant this? not sure.
addAccessControlsForInterface(self.id, ScryptedInterface.ScryptedDevice),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/webrtc').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
addAccessControlsForInterface(sdk.systemManager.getDeviceByName('@scrypted/core').id,
ScryptedInterface.ScryptedDevice,
ScryptedInterface.EngineIOHandler),
]
: [],
...this.storageSettings.values.interfaces.map((deviceInterface: string) => {
const [id, scryptedInterface] = deviceInterface.split('#');
return addAccessControlsForInterface(id, ScryptedInterface.ScryptedDevice, scryptedInterface as ScryptedInterface);
}),
]
};
if (self) {
}
return ret;
}
@@ -72,7 +79,19 @@ export class User extends ScryptedDeviceBase implements Settings, ScryptedUser {
const user = users.find(user => user.username === this.username);
if (!user)
return;
await usersService.addUser(user.username, value.toString(), user.aclId);
const { username, admin } = user;
const nativeId = `user:${username}`;
const aclId = await sdk.deviceManager.onDeviceDiscovered({
providerNativeId: this.nativeId,
name: username.toString(),
nativeId,
interfaces: [
ScryptedInterface.ScryptedUser,
ScryptedInterface.Settings,
],
type: ScryptedDeviceType.Person,
})
await usersService.addUser(user.username, value.toString(), admin ? undefined : aclId);
}
}

View File

@@ -5,7 +5,7 @@
color="primary" icon="mdi-vuetify" border="left">
<template v-slot:prepend>
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
getAlertIcon(alert)
getAlertIcon(alert)
}}</v-icon>
</template>
<div class="caption">{{ alert.title }}</div>
@@ -185,7 +185,8 @@
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
</v-flex>
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<v-flex xs12
v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
<Settings :device="device"></Settings>
</v-flex>
</v-layout>
@@ -240,6 +241,7 @@ import Scene from "../interfaces/Scene.vue";
import TemperatureSetting from "../interfaces/TemperatureSetting.vue";
import PositionSensor from "../interfaces/sensors/PositionSensor.vue";
import DeviceProvider from "../interfaces/DeviceProvider.vue";
import ObjectDetection from "../interfaces/ObjectDetection.vue";
import MixinProvider from "../interfaces/MixinProvider.vue";
import Readme from "../interfaces/Readme.vue";
import Scriptable from "../interfaces/automation/Scriptable.vue";
@@ -286,7 +288,9 @@ const leftInterfaces = [
ScryptedInterface.DeviceProvider,
ScryptedInterface.Readme,
];
const leftAboveInterfaces = [ScryptedInterface.Camera];
const leftAboveInterfaces = [
ScryptedInterface.Camera,
];
const noCardInterfaces = [
ScryptedInterface.Camera,
@@ -294,7 +298,10 @@ const noCardInterfaces = [
ScryptedInterface.Scriptable,
];
const aboveInterfaces = [ScryptedInterface.Scriptable];
const aboveInterfaces = [
ScryptedInterface.ObjectDetection,
ScryptedInterface.Scriptable
];
const cardActionInterfaces = [
ScryptedInterface.OauthClient,
@@ -379,6 +386,8 @@ export default {
Automation,
Program,
Scriptable,
ObjectDetection,
},
mixins: [Mixin],
data() {

View File

@@ -23,6 +23,9 @@ export function createSystemSettingsDevice(systemManager: SystemManager): Scrypt
},
async probe() {
return true;
},
async setMixins() {
},
listen(event, callback) {
let listeners = systemSettings.map(d => d.listen(event, callback));

View File

@@ -24,22 +24,45 @@
Devices</v-btn>
</v-card-actions>
<v-simple-table v-if="discoveredDevices && discoveredDevices.length">
<thead>
<tr>
<th style="width: 10px;"></th>
<th>Discovered</th>
<th></th>
</tr>
</thead>
<tbody v-if="discoveredDevices.length">
<tr v-for="device in discoveredDevices" :key="device.id">
<td>
<v-btn x-small outlined fab color="info" @click="openDeviceAdoptionDialog(device)">
<v-icon>fa-solid
fa-plus</v-icon>
</v-btn>
</td>
<td>
{{ device.name }}
</td>
<td> {{ device.description }}</td>
</tr>
</tbody>
<tbody v-else>
<td></td>
<td>None found.</td>
<td></td>
</tbody>
</v-simple-table>
<v-card-text>These things were created by {{ device.name }}.</v-card-text>
<v-text-field v-model="search" append-icon="search" label="Search" single-line hide-details></v-text-field>
<v-data-table :headers="headers" :items="providerDevices.devices" :items-per-page="10" :search="search">
<v-text-field v-if="managedDevices.devices.length > 10" v-model="search" append-icon="search" label="Search"
single-line hide-details></v-text-field>
<v-data-table v-if="managedDevices.devices.length > 10" :headers="headers" :items="managedDevices.devices"
:items-per-page="10" :search="search">
<template v-slot:[`item.icon`]="{ item }">
<v-icon v-if="!item.nativeId" x-small color="grey">
<v-icon x-small color="grey">
{{ typeToIcon(item.type) }}
</v-icon>
<v-tooltip bottom v-else>
<template v-slot:activator="{ on }">
<v-btn x-small outlined fab v-on="on" color="info" @click="openDeviceAdoptionDialog(item)"><v-icon>fa-solid
fa-plus</v-icon></v-btn>
</template>
<span>Add Discovered Device</span>
</v-tooltip>
</template>
<template v-slot:[`item.name`]="{ item }">
<a v-if="!item.nativeId" link :href="'#' + getDeviceViewPath(item.id)">{{ item.name }}</a>
@@ -48,7 +71,7 @@
</template>
</v-data-table>
<!-- <DeviceGroup v-else :deviceGroup="providerDevices"></DeviceGroup> -->
<DeviceGroup v-else :deviceGroup="managedDevices"></DeviceGroup>
</v-flex>
</template>
<script>
@@ -167,8 +190,8 @@ export default {
});
return ret;
},
providerDevices() {
const currentDevices = this.$store.state.scrypted.devices
managedDevices() {
const devices = this.$store.state.scrypted.devices
.filter(
(id) =>
this.$store.state.systemState[id].providerId.value ===
@@ -181,7 +204,7 @@ export default {
}));
return {
devices: [...this.discoveredDevices || [], ...currentDevices],
devices,
};
},
},

View File

@@ -2,14 +2,23 @@
<v-btn text color="primary" @click="onClick">Login</v-btn>
</template>
<script>
import RPCInterface from "./RPCInterface.vue";
import qs from 'query-string';
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
methods: {
onChange() { },
isIFrame() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
},
onClick: function () {
// https://stackoverflow.com/a/39387533
const windowReference = this.isIFrame() ? window.open(undefined, '_blank') : undefined;
this.rpc()
.getOauthUrl()
.then(data => {
@@ -22,7 +31,9 @@ export default {
u = new URL(redirect_uri);
}
catch (e) {
u = new URL(redirect_uri, window.location.href);
const baseURI = new URL(document.baseURI);
const scryptedRootURI = new URL('../../../../', baseURI);
u = new URL('.' + redirect_uri, scryptedRootURI);
u.hostname = 'localhost';
}
if (u.hostname === 'localhost') {
@@ -40,7 +51,10 @@ export default {
r: window.location.toString(),
});
url.search = qs.stringify(querystring);
window.location = url.toString();
if (windowReference)
windowReference.location = url.toString();
else
window.location = url.toString();
});
}
}

View File

@@ -0,0 +1,91 @@
<template>
<v-sheet :height="600" width="100%" class="d-flex align-center justify-center flex-wrap text-center mx-auto"
@drop="onDrop" @dragover="allowDrop">
<div v-if="!img">Drag and Drop a JPG or PNG to analyze.</div>
<div v-else style="position: relative; height: 100%;">
<img :src="img" style="height: 100%">
<svg v-if="lastDetection" :viewBox="`0 0 ${svgWidth} ${svgHeight}`" ref="svg" style="
top: 0;
left: 0;
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
" v-html="svgContents"></svg>
</div>
</v-sheet>
</template>
<script>
import RPCInterface from "./RPCInterface.vue";
export default {
mixins: [RPCInterface],
data() {
return {
img: null,
lastDetection: null,
}
},
mounted() {
},
computed: {
svgWidth() {
return this.lastDetection?.inputDimensions?.[0] || 1920;
},
svgHeight() {
return this.lastDetection?.inputDimensions?.[1] || 1080;
},
svgContents() {
if (!this.lastDetection) return "";
let contents = "";
for (const detection of this.lastDetection.detections || []) {
if (!detection.boundingBox) continue;
const svgScale = this.svgWidth / 1080;
const sw = 6 * svgScale;
const s = "red";
const x = detection.boundingBox[0];
const y = detection.boundingBox[1];
const w = detection.boundingBox[2];
const h = detection.boundingBox[3];
let t = ``;
let toffset = 0;
if (detection.score && detection.className !== 'motion') {
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}` : '')
t += `<tspan x='${x}' dy='${toffset}em'>${tname}</tspan>`
const fs = 30 * svgScale;
const box = `<rect x="${x}" y="${y}" width="${w}" height="${h}" stroke="${s}" stroke-width="${sw}" fill="none" />
<text x="${x}" y="${y - 5}" font-size="${fs}" dx="0.05em" dy="0.05em" fill="black">${t}</text>
<text x="${x}" y="${y - 5}" font-size="${fs}" fill="white">${t}</text>
`;
contents += box;
}
return contents;
},
},
methods: {
async onDrop(ev) {
ev.preventDefault()
const file = ev.dataTransfer.files[0];
this.img = URL.createObjectURL(file);
const buffer = Buffer.from(await file.arrayBuffer());
const mediaManager = this.$scrypted.mediaManager;
const mo = await mediaManager.createMediaObject(buffer, 'image/*');
const detected = await this.rpc().detectObjects(mo);
this.lastDetection = detected;
},
allowDrop(ev) {
ev.preventDefault();
}
}
}
</script>

View File

@@ -102,6 +102,7 @@ export default {
},
set(value) {
this.rawSettingsGroupName = value;
this.rawSettingsSubgroupName = undefined;
},
},
settingsSubgroupName: {
@@ -110,7 +111,7 @@ export default {
return;
if (this.settingsSubgroups.findIndex(sg => sg === this.rawSettingsSubgroupName) !== -1)
return this.rawSettingsSubgroupName;
return Object.keys(this.settingsSubgroups)?.[0];
return Object.values(this.settingsSubgroups)?.[0];
},
set(value) {
this.rawSettingsSubgroupName = value;

View File

@@ -1,20 +0,0 @@
#!/bin/sh
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
rm -rf all_models
mkdir -p all_models
cd all_models
wget https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel
wget https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt

View File

@@ -1 +0,0 @@
../all_models/MobileNetV2_SSDLite.mlmodel

View File

@@ -1 +0,0 @@
../all_models/coco_labels.txt

View File

@@ -1,19 +1,19 @@
{
"name": "@scrypted/coreml",
"version": "0.0.21",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/coreml",
"version": "0.0.21",
"version": "0.1.2",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.57",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {

View File

@@ -41,5 +41,5 @@
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.21"
"version": "0.1.2"
}

View File

@@ -6,6 +6,10 @@ from predict import PredictPlugin, Prediction, Rectangle
import coremltools as ct
import os
from PIL import Image
import asyncio
import concurrent.futures
predictExecutor = concurrent.futures.ThreadPoolExecutor(2, "CoreML-Predict")
def parse_label_contents(contents: str):
lines = contents.splitlines()
@@ -25,17 +29,19 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
def __init__(self, nativeId: str | None = None):
super().__init__(MIME_TYPE, nativeId=nativeId)
modelPath = os.path.join(os.environ['SCRYPTED_PLUGIN_VOLUME'], 'zip', 'unzipped', 'fs', 'MobileNetV2_SSDLite.mlmodel')
self.model = ct.models.MLModel(modelPath)
labelsFile = self.downloadFile('https://raw.githubusercontent.com/koush/coreml-survival-guide/master/MobileNetV2%2BSSDLite/coco_labels.txt', 'coco_labels.txt')
modelFile = self.downloadFile('https://github.com/koush/coreml-survival-guide/raw/master/MobileNetV2%2BSSDLite/ObjectDetection/ObjectDetection/MobileNetV2_SSDLite.mlmodel', 'MobileNetV2_SSDLite.mlmodel')
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
labels_contents = scrypted_sdk.zip.open(
'fs/coco_labels.txt').read().decode('utf8')
labels_contents = open(labelsFile, 'r').read()
self.labels = parse_label_contents(labels_contents)
self.loop = asyncio.get_event_loop()
# width, height, channels
def get_input_details(self) -> Tuple[int, int, int]:
@@ -44,8 +50,12 @@ class CoreMLPlugin(PredictPlugin, scrypted_sdk.BufferConverter, scrypted_sdk.Set
def get_input_size(self) -> Tuple[float, float]:
return (self.inputwidth, self.inputheight)
def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
async def detect_once(self, input: Image.Image, settings: Any, src_size, cvss):
# run in executor if this is the plugin loop
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': .2 }))
else:
out_dict = self.model.predict({'image': input, 'confidenceThreshold': .2 })
coordinatesList = out_dict['coordinates']

View File

@@ -7,4 +7,5 @@ src
.vscode
dist/*.js
dist/*.txt
face-api.js
HAP-NodeJS
.gitattributes

View File

@@ -10,6 +10,7 @@
"port": 10081,
"request": "attach",
"skipFiles": [
"**/plugin-remote-worker.*",
"<node_internals>/**"
],
"preLaunchTask": "scrypted: deploy+debug",
@@ -19,4 +20,4 @@
"type": "pwa-node"
}
]
}
}

View File

@@ -1,3 +1,3 @@
{
"scrypted.debugHost": "127.0.0.1",
}
}

1
plugins/eufy/README.md Normal file
View File

@@ -0,0 +1 @@
# Eufy Plugin for Scrypted

1334
plugins/eufy/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
plugins/eufy/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@scrypted/eufy",
"description": "Eufy Plugin for Scrypted",
"version": "0.0.1",
"keywords": [
"scrypted",
"plugin",
"eufy",
"camera"
],
"scripts": {
"build": "scrypted-webpack",
"prepublishOnly": "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-webpack": "scrypted-webpack"
},
"scrypted": {
"name": "Eufy",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"Settings"
]
},
"dependencies": {
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@scrypted/h264-repacketizer": "file:../../packages/h264-repacketizer ",
"@types/node": "^18.14.6"
},
"optionalDependencies": {
"eufy-security-client": "^2.4.2"
}
}

306
plugins/eufy/src/main.ts Normal file
View File

@@ -0,0 +1,306 @@
import { listenSingleRtspClient } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp } from '@scrypted/common/src/sdp-utils';
import sdk, { Battery, Camera, Device, DeviceProvider, FFmpegInput, MediaObject, MotionSensor, RequestMediaStreamOptions, RequestPictureOptions, ResponseMediaStreamOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import eufy, { CaptchaOptions, EufySecurity, P2PClientProtocol, P2PConnectionType } from 'eufy-security-client';
import { startRtpForwarderProcess } from '../../webrtc/src/rtp-forwarders';
import { Deferred } from '@scrypted/common/src/deferred';
import { Writable } from 'stream';
import { LocalLivestreamManager } from './stream';
const { deviceManager, mediaManager, systemManager } = sdk;
class EufyCamera extends ScryptedDeviceBase implements VideoCamera, MotionSensor {
client: EufySecurity;
device: eufy.Camera;
constructor(nativeId: string, client: EufySecurity, device: eufy.Camera) {
super(nativeId);
this.client = client;
this.device = device;
this.setupMotionDetection();
}
setupMotionDetection() {
const handle = (device: eufy.Device, state: boolean) => {
this.motionDetected = state;
};
this.device.on('motion detected', handle);
this.device.on('person detected', handle);
this.device.on('pet detected', handle);
this.device.on('vehicle detected', handle);
this.device.on('dog detected', handle);
this.device.on('radar motion detected', handle);
}
getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
return this.createVideoStream(options);
}
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
return [
{
container: 'rtsp',
id: 'p2p',
name: 'P2P',
video: {
codec: 'h264',
},
audio: {
codec: 'aac',
},
tool: 'scrypted',
userConfigurable: false,
},
{
container: 'rtsp',
id: 'p2p-low',
name: 'P2P (Low Resolution)',
video: {
codec: 'h264',
width: 1280,
height: 720,
},
audio: {
codec: 'aac',
},
tool: 'scrypted',
userConfigurable: false,
},
];
}
async createVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
const livestreamManager = new LocalLivestreamManager(options.id, this.client, this.device, this.console);
const kill = new Deferred<void>();
kill.promise.finally(() => {
this.console.log('video stream exited');
livestreamManager.stopLocalLiveStream();
});
const rtspServer = await listenSingleRtspClient();
rtspServer.rtspServerPromise.then(async rtsp => {
kill.promise.finally(() => rtsp.client.destroy());
rtsp.client.on('close', () => kill.resolve());
try {
const process = await startRtpForwarderProcess(this.console, {
inputArguments: [
'-f', 'h264', '-i', 'pipe:4',
'-f', 'aac', '-i', 'pipe:5',
]
}, {
video: {
onRtp: rtp => {
if (videoTrack)
rtsp.sendTrack(videoTrack.control, rtp, false);
},
encoderArguments: [
'-vcodec', 'copy',
]
},
audio: {
onRtp: rtp => {
if (audioTrack)
rtsp.sendTrack(audioTrack.control, rtp, false);
},
encoderArguments: [
'-acodec', 'copy',
'-rtpflags', 'latm',
]
}
});
process.killPromise.finally(() => kill.resolve());
kill.promise.finally(() => process.kill());
let parsedSdp: ReturnType<typeof parseSdp>;
let videoTrack: typeof parsedSdp.msections[0]
let audioTrack: typeof parsedSdp.msections[0]
process.sdpContents.then(async sdp => {
sdp = addTrackControls(sdp);
rtsp.sdp = sdp;
parsedSdp = parseSdp(sdp);
videoTrack = parsedSdp.msections.find(msection => msection.type === 'video');
audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio');
await rtsp.handlePlayback();
});
const proxyStream = await livestreamManager.getLocalLivestream();
proxyStream.videostream.pipe(process.cp.stdio[4] as Writable);
proxyStream.audiostream.pipe((process.cp.stdio as any)[5] as Writable);
}
catch (e) {
rtsp.client.destroy();
}
});
const input: FFmpegInput = {
url: rtspServer.url,
mediaStreamOptions: options,
inputArguments: [
'-i', rtspServer.url,
]
};
return mediaManager.createFFmpegMediaObject(input);
}
}
class EufyPlugin extends ScryptedDeviceBase implements DeviceProvider, Settings {
client: EufySecurity;
devices = new Map<string, any>();
storageSettings = new StorageSettings(this, {
country: {
title: 'Country',
defaultValue: 'US',
},
email: {
title: 'Email',
onPut: async () => this.tryLogin(),
},
password: {
title: 'Password',
type: 'password',
onPut: async () => this.tryLogin(),
},
twoFactorCode: {
title: 'Two Factor Code',
description: 'Optional: If 2FA is enabled on your account, enter the code sent to your email or phone number.',
onPut: async (oldValue, newValue) => {
await this.tryLogin(newValue);
},
noStore: true,
},
captcha: {
title: 'Captcha',
description: 'Optional: If a captcha request is recieved, enter the code in the image.',
onPut: async (oldValue, newValue) => {
await this.tryLogin(undefined, newValue);
},
noStore: true,
},
captchaId: {
title: 'Captcha Id',
hide: true,
}
});
constructor() {
super();
this.tryLogin()
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
async tryLogin(twoFactorCode?: string, captchaCode?: string) {
this.log.clearAlerts();
if (!this.storageSettings.values.email || !this.storageSettings.values.email) {
this.log.a('Enter your Eufy email and password to complete setup.');
throw new Error('Eufy email and password are missing.');
}
await this.initializeClient();
var captchaOptions: CaptchaOptions = undefined
if (captchaCode) {
captchaOptions = {
captchaCode: captchaCode,
captchaId: this.storageSettings.values.captchaId,
}
}
await this.client.connect({ verifyCode: twoFactorCode, captcha: captchaOptions, force: false });
}
private async initializeClient() {
const config = {
username: this.storageSettings.values.email,
password: this.storageSettings.values.password,
country: this.storageSettings.values.country,
language: 'en',
p2pConnectionSetup: P2PConnectionType.QUICKEST,
pollingIntervalMinutes: 10,
eventDurationSeconds: 10
}
this.client = await EufySecurity.initialize(config);
this.client.on('device added', this.deviceAdded.bind(this));
this.client.on('station added', this.stationAdded.bind(this));
this.client.on('tfa request', () => {
this.log.a('Login failed: 2FA is enabled, check your email or texts for your code, then enter it into the Two Factor Code setting to conplete login.');
});
this.client.on('captcha request', (id, captcha) => {
this.log.a(`Login failed: Captcha was requested, fill out the Captcha setting to conplete login. </br> <img src="${captcha}" />`);
this.storageSettings.putSetting('captchaId', id);
});
this.client.on('connect', () => {
this.console.debug(`[${this.name}] (${new Date().toLocaleString()}) Client connected.`);
this.log.clearAlerts();
});
this.client.on('push connect', () => {
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Connected.`);
});
this.client.on('push close', () => {
this.console.log(`[${this.name}] (${new Date().toLocaleString()}) Push Closed.`);
});
}
private async deviceAdded(eufyDevice: eufy.Device) {
if (!eufyDevice.isCamera) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Ignoring unsupported discovered device: `, eufyDevice.getName(), eufyDevice.getModel());
return;
}
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device discovered: `, eufyDevice.getName(), eufyDevice.getModel());
const nativeId = eufyDevice.getSerial();
const interfaces = [
ScryptedInterface.VideoCamera
];
if (eufyDevice.hasBattery())
interfaces.push(ScryptedInterface.Battery);
if (eufyDevice.hasProperty('motionDetection'))
interfaces.push(ScryptedInterface.MotionSensor);
const device: Device = {
info: {
model: eufyDevice.getModel(),
manufacturer: 'Eufy',
firmware: eufyDevice.getSoftwareVersion(),
serialNumber: nativeId
},
nativeId,
name: eufyDevice.getName(),
type: ScryptedDeviceType.Camera,
interfaces,
};
this.devices.set(nativeId, new EufyCamera(nativeId, this.client, eufyDevice as eufy.Camera))
await deviceManager.onDeviceDiscovered(device);
}
private async stationAdded(station: eufy.Station) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Station discovered: `, station.getName(), station.getModel(), `but stations are not currently supported.`);
}
async getDevice(nativeId: string): Promise<any> {
return this.devices.get(nativeId);
}
async releaseDevice(id: string, nativeId: string) {
this.console.info(`[${this.name}] (${new Date().toLocaleString()}) Device with id '${nativeId}' was removed.`);
}
}
export default new EufyPlugin();

227
plugins/eufy/src/stream.ts Normal file
View File

@@ -0,0 +1,227 @@
// Based off of https://github.com/homebridge-eufy-security/plugin/blob/master/src/plugin/controller/LocalLivestreamManager.ts
import { Camera, CommandData, CommandName, CommandType, Device, DeviceType, EufySecurity, isGreaterEqualMinVersion, P2PClientProtocol, ParamType, Station, StreamMetadata, VideoCodec } from 'eufy-security-client';
import { EventEmitter, Readable } from 'stream';
type StationStream = {
station: Station;
device: Device;
metadata: StreamMetadata;
videostream: Readable;
audiostream: Readable;
createdAt: number;
};
export class LocalLivestreamManager extends EventEmitter {
private stationStream: StationStream | null;
private console: Console;
private livestreamStartedAt: number | null;
private livestreamIsStarting = false;
private readonly id: string;
private readonly client: EufySecurity;
private readonly device: Camera;
private station: Station;
private p2pSession: P2PClientProtocol;
constructor(id: string, client: EufySecurity, device: Camera, console: Console) {
super();
this.id = id;
this.console = console;
this.client = client;
this.device = device;
this.client.getStation(this.device.getStationSerial()).then( (station) => {
this.station = station;
this.p2pSession = new P2PClientProtocol(station.getRawStation(), this.client.getApi(), station.getIPAddress());
this.p2pSession.on("livestream started", (channel: number, metadata: StreamMetadata, videostream: Readable, audiostream: Readable) => {
this.onStationLivestreamStart(station, device, metadata, videostream, audiostream);
});
this.p2pSession.on("livestream stopped", (channel: number) => {
this.onStationLivestreamStop(station, device);
});
this.p2pSession.on("livestream error", (channel: number, error: Error) => {
this.stopLivestream();
});
});
this.stationStream = null;
this.livestreamStartedAt = null;
this.initialize();
}
private initialize() {
if (this.stationStream) {
this.stationStream.audiostream.unpipe();
this.stationStream.audiostream.destroy();
this.stationStream.videostream.unpipe();
this.stationStream.videostream.destroy();
}
this.stationStream = null;
this.livestreamStartedAt = null;
}
public async getLocalLivestream(): Promise<StationStream> {
this.console.debug(this.device.getName(), this.id, 'New instance requests livestream.');
if (this.stationStream) {
const runtime = (Date.now() - this.livestreamStartedAt!) / 1000;
this.console.debug(this.device.getName(), this.id, 'Using livestream that was started ' + runtime + ' seconds ago.');
return this.stationStream;
} else {
return await this.startAndGetLocalLiveStream();
}
}
private async startAndGetLocalLiveStream(): Promise<StationStream> {
return new Promise((resolve, reject) => {
this.console.debug(this.device.getName(), this.id, 'Start new station livestream...');
if (!this.livestreamIsStarting) { // prevent multiple stream starts from eufy station
this.livestreamIsStarting = true;
this.startStationLivestream();
} else {
this.console.debug(this.device.getName(), this.id, 'stream is already starting. waiting...');
}
this.once('livestream start', async () => {
if (this.stationStream !== null) {
this.console.debug(this.device.getName(), this.id, 'New livestream started.');
this.livestreamIsStarting = false;
resolve(this.stationStream);
} else {
reject('no started livestream found');
}
});
});
}
private async startStationLivestream(videoCodec: VideoCodec = VideoCodec.H264): Promise<void> {
const commandData: CommandData = {
name: CommandName.DeviceStartLivestream,
value: videoCodec
};
this.console.debug(this.device.getName(), this.id, `Sending start livestream command to station ${this.station.getSerial()}`);
const rsa_key = this.p2pSession.getRSAPrivateKey();
if (this.device.isSoloCameras() || this.device.getDeviceType() === DeviceType.FLOODLIGHT_CAMERA_8423 || this.device.isWiredDoorbellT8200X()) {
this.console.debug(this.device.getName(), this.id, `Using CMD_DOORBELL_SET_PAYLOAD (1) for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithStringPayload({
commandType: CommandType.CMD_DOORBELL_SET_PAYLOAD,
value: JSON.stringify({
"commandType": ParamType.COMMAND_START_LIVESTREAM,
"data": {
"accountId": this.station.getRawStation().member.admin_user_id,
"encryptkey": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
"streamtype": videoCodec
}
}),
channel: this.device.getChannel()
}, {
command: commandData
});
} else if (this.device.isWiredDoorbell() || (this.device.isFloodLight() && this.device.getDeviceType() !== DeviceType.FLOODLIGHT) || this.device.isIndoorCamera() || (this.device.getSerial().startsWith("T8420") && isGreaterEqualMinVersion("2.0.4.8", this.station.getSoftwareVersion()))) {
this.console.debug(this.device.getName(), this.id, `Using CMD_DOORBELL_SET_PAYLOAD (2) for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithStringPayload({
commandType: CommandType.CMD_DOORBELL_SET_PAYLOAD,
value: JSON.stringify({
"commandType": ParamType.COMMAND_START_LIVESTREAM,
"data": {
"account_id": this.station.getRawStation().member.admin_user_id,
"encryptkey": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
"streamtype": videoCodec
}
}),
channel: this.device.getChannel()
}, {
command: commandData
});
} else {
if ((Device.isIntegratedDeviceBySn(this.station.getSerial()) || !isGreaterEqualMinVersion("2.0.9.7", this.station.getSoftwareVersion())) && (!this.station.getSerial().startsWith("T8420") || !isGreaterEqualMinVersion("1.0.0.25", this.station.getSoftwareVersion()))) {
this.console.debug(this.device.getName(), this.id, `Using CMD_START_REALTIME_MEDIA for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithInt({
commandType: CommandType.CMD_START_REALTIME_MEDIA,
value: this.device.getChannel(),
strValue: rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
channel: this.device.getChannel()
}, {
command: commandData
});
} else {
this.console.debug(this.device.getName(), this.id, `Using CMD_SET_PAYLOAD for station ${this.station.getSerial()} (main_sw_version: ${this.station.getSoftwareVersion()})`);
await this.p2pSession.sendCommandWithStringPayload({
commandType: CommandType.CMD_SET_PAYLOAD,
value: JSON.stringify({
"account_id": this.station.getRawStation().member.admin_user_id,
"cmd": CommandType.CMD_START_REALTIME_MEDIA,
"mValue3": CommandType.CMD_START_REALTIME_MEDIA,
"payload": {
"ClientOS": "Android",
"key": rsa_key?.exportKey("components-public").n.slice(1).toString("hex"),
"streamtype": videoCodec === VideoCodec.H264 ? 1 : 2,
}
}),
channel: this.device.getChannel()
}, {
command: commandData
});
}
}
}
public stopLocalLiveStream(): void {
this.console.debug(this.device.getName(), this.id, 'Stopping station livestream.');
this.stopLivestream();
this.initialize();
}
private async stopLivestream(): Promise<void> {
const commandData: CommandData = {
name: CommandName.DeviceStopLivestream
};
this.console.debug(this.device.getName(), this.id, `Sending stop livestream command to station ${this.station.getSerial()}`);
await this.p2pSession.sendCommandWithInt({
commandType: CommandType.CMD_STOP_REALTIME_MEDIA,
value: this.device.getChannel(),
channel: this.device.getChannel()
}, {
command: commandData
});
}
private onStationLivestreamStop(station: Station, device: Device) {
if (device.getSerial() === this.device.getSerial()) {
this.console.info(this.id + ' - ' + station.getName() + ' station livestream for ' + device.getName() + ' has stopped.');
this.initialize();
}
}
private async onStationLivestreamStart(
station: Station,
device: Device,
metadata: StreamMetadata,
videostream: Readable,
audiostream: Readable,
) {
if (device.getSerial() === this.device.getSerial()) {
if (this.stationStream) {
const diff = (Date.now() - this.stationStream.createdAt) / 1000;
if (diff < 5) {
this.console.warn(this.device.getName(), this.id, 'Second livestream was started from station. Ignore.');
return;
}
}
this.initialize(); // important to prevent unwanted behaviour when the eufy station emits the 'livestream start' event multiple times
this.console.info(this.id + ' - ' + station.getName() + ' station livestream (P2P session) for ' + device.getName() + ' has started.');
this.livestreamStartedAt = Date.now();
const createdAt = Date.now();
this.stationStream = {station, device, metadata, videostream, audiostream, createdAt};
this.console.debug(this.device.getName(), this.id, 'Stream metadata: ' + JSON.stringify(this.stationStream.metadata));
this.emit('livestream start');
}
}
}

View File

@@ -10,4 +10,4 @@
"include": [
"src/**/*"
]
}
}

View File

@@ -174,7 +174,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
async createDevice(settings: DeviceCreatorSettings, nativeId?: ScryptedNativeId): Promise<string> {
nativeId ||= randomBytes(4).toString('hex');
const name = settings.newCamera.toString();
const name = settings.newCamera?.toString() || 'New Camera';
await this.updateDevice(nativeId, name, this.getInterfaces());
return nativeId;
}
@@ -208,6 +208,7 @@ export abstract class CameraProviderBase<T extends ResponseMediaStreamOptions> e
name,
interfaces,
type: type || ScryptedDeviceType.Camera,
info: deviceManager.getNativeIds().includes(nativeId) ? deviceManager.getDeviceState(nativeId)?.info : undefined,
});
}

View File

@@ -1,15 +0,0 @@
# Google Cloud Text to Speech plugin
## npm commands
* npm run scrypted-webpack
* npm run scrypted-deploy <ipaddress>
* npm run scrypted-debug <ipaddress>
## scrypted distribution via npm
1. Ensure package.json is set up properly for publishing on npm.
2. npm publish
## Visual Studio Code configuration
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
* Launch Scrypted Debugger from the launch menu.

View File

@@ -1,141 +0,0 @@
{
"name": "@scrypted/google-cloud-tts",
"version": "0.0.21",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/google-cloud-tts",
"version": "0.0.21",
"hasInstallScript": true,
"dependencies": {
"axios": "^0.24.0"
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^17.0.8"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.0.199",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.16.7",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"webpack": "^5.59.0"
},
"bin": {
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-readme": "bin/scrypted-readme.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^16.11.1",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"../sdk": {
"extraneous": true
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
},
"node_modules/@types/node": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
"dev": true
},
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
}
},
"dependencies": {
"@scrypted/sdk": {
"version": "file:../../sdk",
"requires": {
"@babel/preset-typescript": "^7.16.7",
"@types/node": "^16.11.1",
"@types/stringify-object": "^4.0.0",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^8.2.3",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.13.8",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"stringify-object": "^3.3.0",
"tmp": "^0.2.1",
"ts-node": "^10.4.0",
"typedoc": "^0.22.8",
"typescript-json-schema": "^0.50.1",
"webpack": "^5.59.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
"@types/node": {
"version": "17.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.8.tgz",
"integrity": "sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==",
"dev": true
},
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"follow-redirects": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
"integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
}
}
}

View File

@@ -1,95 +0,0 @@
// https://developer.scrypted.app/#getting-started
import axios from 'axios';
import sdk, { BufferConverter, ScryptedDeviceBase, Settings, Setting } from "@scrypted/sdk";
class GoogleCloudTts extends ScryptedDeviceBase implements BufferConverter, Settings {
constructor() {
super();
this.fromMimeType = 'text/plain';
this.toMimeType = 'audio/mpeg';
if (!this.getApiKey())
this.log.a('API key missing.');
}
getApiKey() {
const apiKey = this.storage.getItem('api_key');
return apiKey;
}
async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer> {
const voice_name = this.storage.getItem("voice_name") || "en-GB-Standard-A";
const voice_gender = this.storage.getItem("voice_gender") || "FEMALE";
const voice_language_code = this.storage.getItem("voice_language_code") || "en-GB";
const from = Buffer.from(data);
var json = {
"input": {
"text": from.toString()
},
"voice": {
"languageCode": voice_language_code,
"name": voice_name,
"ssmlGender": voice_gender
},
"audioConfig": {
"audioEncoding": "MP3"
}
};
var result = await axios.post(`https://texttospeech.googleapis.com/v1/text:synthesize?key=${this.getApiKey()}`, json);
console.log(JSON.stringify(result.data, null, 2));
const buffer = Buffer.from(result.data.audioContent, 'base64');
return buffer;
}
voices: any;
async getSettings(): Promise<Setting[]> {
const ret: Setting[] = [
{
title: 'API Key',
description: 'API Key used by Google Cloud TTS.',
key: 'api_key',
value: this.storage.getItem('api_key'),
}
];
if (!this.getApiKey())
return ret;
try {
if (!this.voices) {
const response = await axios.get(`https://texttospeech.googleapis.com/v1/voices?key=${this.getApiKey()}`)
this.voices = response.data;
}
}
catch (e) {
this.log.a('Error retrieving settings from Google Cloud Text to Speech. Is your API Key correct?');
return ret;
}
ret.push({
title: "Voice",
choices: this.voices.voices.map(voice => voice.name),
key: "voice",
value: this.storage.getItem("voice_name"),
});
return ret;
}
async putSetting(key: string, value: string | number | boolean) {
if (key !== 'voice') {
this.storage.setItem(key, value.toString());
return;
}
const found = this.voices.voices.find((voice: any) => voice.name === value);
if (!found) {
console.error('Voice not found.');
return;
}
localStorage.setItem('voice_name', found.name);
localStorage.setItem('voice_language_code', found.languageCodes[0]);
localStorage.setItem('voice_gender', found.ssmlGender);
}
}
export default new GoogleCloudTts();

File diff suppressed because it is too large Load Diff

View File

@@ -39,18 +39,15 @@
"dependencies": {
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@googleapis/smartdevicemanagement": "^0.2.0",
"axios": "^0.21.1",
"@googleapis/smartdevicemanagement": "^1.0.0",
"axios": "^1.3.4",
"client-oauth2": "^4.3.3",
"lodash": "^4.17.21",
"query-string": "^7.0.0",
"url-parse": "^1.5.1"
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/debug": "^4.1.5",
"@types/lodash": "^4.14.168",
"@types/node": "^14.17.11",
"@types/url-parse": "^1.4.3"
"@types/debug": "^4.1.7",
"@types/lodash": "^4.14.191",
"@types/node": "^18.14.1"
},
"version": "0.0.95"
"version": "0.0.96"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "appengine-hello-world",
"description": "Simple Hello World Node.js sample for Google App Engine Standard Environment.",
"version": "0.0.3",
"version": "0.0.4",
"private": true,
"license": "Apache-2.0",
"author": "Google Inc.",
@@ -10,24 +10,24 @@
"url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git"
},
"engines": {
"node": ">=14.0.0"
"node": ">=18.0.0"
},
"scripts": {
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"@google-cloud/datastore": "^6.5.0",
"axios": "^0.21.1",
"@google-cloud/datastore": "^7.3.2",
"axios": "^1.3.4",
"client-oauth2": "^4.3.3",
"express": "^4.17.1",
"typescript": "^4.4.2"
"express": "^4.18.2",
"typescript": "^4.9.5"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^16.7.10",
"mocha": "^9.0.0",
"supertest": "^6.0.0",
"ts-node": "^10.2.1"
"@types/express": "^4.17.17",
"@types/node": "^18.14.1",
"mocha": "^10.2.0",
"supertest": "^6.3.3",
"ts-node": "^10.9.1"
}
}

View File

@@ -1,12 +1,12 @@
import sdk, { ScryptedDeviceBase, DeviceManifest, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MotionSensor, OauthClient, Refresh, ScryptedDeviceType, ScryptedInterface, Setting, Settings, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera, BinarySensor, DeviceInformation, RTCAVSignalingSetup, Camera, PictureOptions, ObjectsDetected, ObjectDetector, ObjectDetectionTypes, FFmpegInput, RequestMediaStreamOptions, Readme, RTCSignalingChannel, RTCSessionControl, RTCSignalingSession, ResponseMediaStreamOptions, RTCSignalingSendIceCandidate, ScryptedMimeTypes, MediaStreamUrl, TemperatureCommand, OnOff } from '@scrypted/sdk';
import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling';
import { sleep } from '@scrypted/common/src/sleep';
import sdk, { BinarySensor, Camera, DeviceInformation, DeviceManifest, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, HumiditySensor, MediaObject, MediaStreamUrl, MotionSensor, OauthClient, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, PictureOptions, Readme, Refresh, RequestMediaStreamOptions, ResponseMediaStreamOptions, RTCAVSignalingSetup, RTCSessionControl, RTCSignalingChannel, RTCSignalingSendIceCandidate, RTCSignalingSession, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, TemperatureCommand, TemperatureSetting, TemperatureUnit, Thermometer, ThermostatMode, VideoCamera } from '@scrypted/sdk';
import axios from 'axios';
import ClientOAuth2 from 'client-oauth2';
import { randomBytes } from 'crypto';
import fs from 'fs';
import throttle from 'lodash/throttle';
import qs from 'query-string';
import querystring from "querystring";
import { URL } from 'url';
const { deviceManager, mediaManager, endpointManager, systemManager } = sdk;
@@ -334,11 +334,12 @@ for (const [k, v] of setpointMap.entries()) {
setpointReverseMap.set(v, k);
}
class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Settings, Refresh {
class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Settings, Refresh, OnOff {
device: any;
provider: GoogleSmartDeviceAccess;
executeCommandSetMode: any = undefined;
executeCommandSetCelsius: any = undefined;
executeCommandSetTimer: any = undefined;
executeThrottle = throttle(async () => {
if (this.executeCommandSetCelsius) {
@@ -365,6 +366,12 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
this.console.log('executeCommandSetCelsius', command);
return this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, command);
}
if (this.executeCommandSetTimer) {
const command = this.executeCommandSetTimer;
this.executeCommandSetTimer = undefined;
this.console.log('executeCommandSetTimer', command);
return this.provider.authPost(`/devices/${this.nativeId}:executeCommand`, command);
}
}, 12000)
constructor(provider: GoogleSmartDeviceAccess, device: any) {
@@ -425,6 +432,33 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
// not supported by API. throw?
}
async turnOff(): Promise<void> {
// You can't turn the fan off when the HVAC unit is currently running.
if (this.thermostatActiveMode !== ThermostatMode.Off) {
this.on = false;
await this.refresh(null, true); // Refresh the state to turn the fan switch back to active.
return;
}
this.executeCommandSetTimer = {
command: 'sdm.devices.commands.Fan.SetTimer',
params: {
timerMode: 'OFF',
},
}
await this.executeThrottle();
await this.refresh(null, true);
}
async turnOn(): Promise<void> {
this.executeCommandSetTimer = {
command: 'sdm.devices.commands.Fan.SetTimer',
params: {
timerMode: 'ON',
},
}
await this.executeThrottle();
await this.refresh(null, true);
}
reload() {
const device = this.device;
@@ -478,6 +512,9 @@ class NestThermostat extends ScryptedDeviceBase implements HumiditySensor, Therm
setpoint,
availableModes: modes,
}
// Set Fan Status
this.on = this.thermostatActiveMode !== ThermostatMode.Off || device.traits?.['sdm.devices.traits.Fan']?.timerMode === "ON";
}
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
@@ -764,7 +801,7 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
response_type: 'code',
scope: 'https://www.googleapis.com/auth/sdm.service',
}
return `${this.authorizationUri}?${qs.stringify(params)}`;
return `${this.authorizationUri}?${querystring.stringify(params)}`;
}
async onOauthCallback(callbackUrl: string) {
const cb = new URL(callbackUrl);
@@ -833,6 +870,7 @@ export class GoogleSmartDeviceAccess extends ScryptedDeviceBase implements Oauth
ScryptedInterface.HumiditySensor,
ScryptedInterface.Thermometer,
ScryptedInterface.Settings,
ScryptedInterface.OnOff
],
info,
})

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.120",
"version": "0.0.124",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/hikvision",
"version": "0.0.120",
"version": "0.0.124",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/hikvision",
"version": "0.0.120",
"version": "0.0.124",
"description": "Hikvision Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -38,16 +38,10 @@
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/highland": "^2.12.14",
"@types/lodash": "^4.14.172",
"@types/multiparty": "^0.0.33",
"@types/node": "^16.9.1",
"@types/xml2js": "^0.4.9",
"axios": "^0.23.0",
"highland": "^2.13.5",
"lodash": "^4.17.21",
"multiparty": "^4.2.2",
"net-keepalive": "^3.0.0",
"xml2js": "^0.4.23"
}
}

View File

@@ -1,10 +1,6 @@
import AxiosDigestAuth from '@koush/axios-digest-auth';
import { IncomingMessage } from 'http';
import https from 'https';
export const hikvisionHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
import { getDeviceInfo, hikvisionHttpsAgent } from './probe';
export function getChannel(channel: string) {
return channel || '101';
@@ -44,31 +40,18 @@ export class HikvisionCameraAPI {
}
async getDeviceInfo() {
try {
const response = await this.digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/ISAPI/System/deviceInfo`,
});
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
return {
deviceModel,
deviceName,
serialNumber,
macAddress,
firmwareVersion,
};
}
catch (e) {
if (e?.response?.data?.includes('notActivated'))
throw new Error(`Camera must be first be activated at http://${this.ip}.`)
throw e;
}
return getDeviceInfo(this.digestAuth, this.ip);
}
async checkTwoWayAudio() {
const response = await this.digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${this.ip}/ISAPI/System/TwoWayAudio/channels`,
});
return (response.data as string).includes('Speaker');
}
async checkDeviceModel(): Promise<string> {

View File

@@ -1,13 +1,13 @@
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, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, VideoStreamOptions } from "@scrypted/sdk";
import sdk, { Camera, DeviceCreatorSettings, DeviceInformation, FFmpegInput, Intercom, MediaObject, MediaStreamOptions, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting } from "@scrypted/sdk";
import child_process, { ChildProcess } from 'child_process';
import { PassThrough, Readable } from "stream";
import { sleep } from "../../../common/src/sleep";
import xml2js from 'xml2js';
import { OnvifIntercom } from "../../onvif/src/onvif-intercom";
import { RtspProvider, RtspSmartCamera, UrlMediaStreamOptions } from "../../rtsp/src/rtsp";
import { getChannel, HikvisionCameraAPI, HikvisionCameraEvent, hikvisionHttpsAgent } from "./hikvision-camera-api";
import xml2js from 'xml2js';
import { HikvisionCameraAPI, HikvisionCameraEvent } from "./hikvision-camera-api";
import { hikvisionHttpsAgent } from './probe';
const { mediaManager } = sdk;
@@ -26,19 +26,29 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
constructor(nativeId: string, provider: RtspProvider) {
super(nativeId, provider);
this.updateManagementUrl();
this.updateDeviceInfo();
}
updateManagementUrl() {
async updateDeviceInfo() {
const ip = this.storage.getItem('ip');
if (!ip)
return;
const info = this.info || {};
const managementUrl = `http://${ip}`;
if (info.managementUrl !== managementUrl) {
info.managementUrl = managementUrl;
this.info = info;
const info: DeviceInformation = {
...this.info,
managementUrl,
ip,
manufacturer: 'Hikvision',
};
const client = this.getClient();
const deviceInfo = await client.getDeviceInfo().catch(() => { });
if (deviceInfo) {
info.model = deviceInfo.deviceModel;
info.mac = deviceInfo.macAddress;
info.firmware = deviceInfo.firmwareVersion;
info.serialNumber = deviceInfo.serialNumber;
}
this.info = info;
}
async listenEvents() {
@@ -284,7 +294,7 @@ class HikvisionCamera extends RtspSmartCamera implements Camera, Intercom {
this.provider.updateDevice(this.nativeId, this.name, interfaces, type);
this.updateManagementUrl();
this.updateDeviceInfo();
}
async getOtherSettings(): Promise<Setting[]> {
@@ -526,9 +536,10 @@ class HikvisionProvider extends RtspProvider {
const username = settings.username?.toString();
const password = settings.password?.toString();
const skipValidate = settings.skipValidate === 'true';
let twoWayAudio: string;
if (!skipValidate) {
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
try {
const api = new HikvisionCameraAPI(httpAddress, username, password, this.console);
const deviceInfo = await api.getDeviceInfo();
settings.newCamera = deviceInfo.deviceName;
@@ -542,6 +553,15 @@ class HikvisionProvider extends RtspProvider {
this.console.error('Error adding Hikvision camera', e);
throw e;
}
try {
if (await api.checkTwoWayAudio()) {
twoWayAudio = 'Hikvision';
}
}
catch (e) {
this.console.warn('Error probing two way audio', e);
}
}
settings.newCamera ||= 'Hikvision Camera';
@@ -553,6 +573,8 @@ class HikvisionProvider extends RtspProvider {
device.putSetting('password', password);
device.setIPAddress(settings.ip?.toString());
device.setHttpPortOverride(settings.httpPort?.toString());
if (twoWayAudio)
device.putSetting('twoWayAudio', twoWayAudio);
return nativeId;
}

View File

@@ -0,0 +1,34 @@
import https from 'https';
import AxiosDigestAuth from '@koush/axios-digest-auth';
export const hikvisionHttpsAgent = new https.Agent({
rejectUnauthorized: false,
});
export async function getDeviceInfo(digestAuth: AxiosDigestAuth, address: string) {
try {
const response = await digestAuth.request({
httpsAgent: hikvisionHttpsAgent,
method: "GET",
responseType: 'text',
url: `http://${address}/ISAPI/System/deviceInfo`,
});
const deviceModel = response.data.match(/>(.*?)<\/model>/)?.[1];
const deviceName = response.data.match(/>(.*?)<\/deviceName>/)?.[1];
const serialNumber = response.data.match(/>(.*?)<\/serialNumber>/)?.[1];
const macAddress = response.data.match(/>(.*?)<\/macAddress>/)?.[1];
const firmwareVersion = response.data.match(/>(.*?)<\/firmwareVersion>/)?.[1];
return {
deviceModel,
deviceName,
serialNumber,
macAddress,
firmwareVersion,
};
}
catch (e) {
if (e?.response?.data?.includes('notActivated'))
throw new Error(`Camera must be first be activated at http://${address}.`)
throw e;
}
}

View File

@@ -32,6 +32,11 @@ export function nextSequenceNumber(current: number, increment = 1) {
return (current + increment + 0x10000) % 0x10000;
}
const maxRtpTimestamp = BigInt(0xFFFFFFFF);
export function addRtpTimestamp(current: number, adjust: number) {
return Number(maxRtpTimestamp & (BigInt(current) + BigInt(adjust)));
}
export function isNextSequenceNumber(current: number, next: number) {
return nextSequenceNumber(current) === next;
}

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