Compare commits

...

622 Commits

Author SHA1 Message Date
Koushik Dutta
bb184247d0 server: fix deleted device leak 2023-04-28 08:28:40 -07:00
Koushik Dutta
dbc45173ae postbeta 2023-04-28 08:02:00 -07:00
Koushik Dutta
95a23b2882 postrelease 2023-04-28 07:34:50 -07:00
Koushik Dutta
212883e84b server: probe one off discovered devices after creation 2023-04-28 07:34:21 -07:00
Koushik Dutta
1200537d62 cloud: support default login 2023-04-27 23:42:54 -07:00
Koushik Dutta
5f6adc9449 predict: publish 2023-04-27 21:53:50 -07:00
Koushik Dutta
7d17236ca7 server: fix prepublishOnly script 2023-04-27 10:31:13 -07:00
Koushik Dutta
028401362a postrelease 2023-04-27 10:30:59 -07:00
Koushik Dutta
69927be4f4 rebroadcast: publish beta 2023-04-26 22:51:34 -07:00
Koushik Dutta
ffee1c5cc2 predict: publish 2023-04-26 22:51:28 -07:00
Koushik Dutta
ebc3a03e2c postrelease 2023-04-26 22:47:50 -07:00
Koushik Dutta
4246e3c476 server: filter link local addresses 2023-04-26 22:47:33 -07:00
Koushik Dutta
3fce0838f1 Merge branch 'main' of github.com:koush/scrypted 2023-04-26 18:40:27 -07:00
Koushik Dutta
2609e301fe python-codecs: fix gray conversion 2023-04-26 18:40:22 -07:00
Koushik Dutta
f4737bf2ac docker: fix stupid bash/zsh issue 2023-04-26 10:22:55 -07:00
Koushik Dutta
fc102aa526 postbeta 2023-04-26 09:56:27 -07:00
Koushik Dutta
9ef33e156f docker: pass through /dev/dri in compose 2023-04-26 09:40:07 -07:00
Koushik Dutta
881865a0cb docker: add intel opencl driver 2023-04-26 09:22:16 -07:00
Koushik Dutta
be5643cc53 openvino: fix bufferconvertor 2023-04-25 22:35:41 -07:00
Koushik Dutta
7e6eba1596 openvino: initial release 2023-04-25 21:56:07 -07:00
Koushik Dutta
27dde776a6 rebroadcast: further settings cleanups 2023-04-25 18:46:38 -07:00
Koushik Dutta
b24159a22a rebroadcast: strip out legacy containers 2023-04-25 18:32:11 -07:00
Koushik Dutta
b6c242b9d5 postrelease 2023-04-25 14:11:58 -07:00
Koushik Dutta
2fbaa12caa core: support selecting interfaces 2023-04-25 14:10:04 -07:00
Koushik Dutta
eb5a497e82 prebeta 2023-04-25 14:04:56 -07:00
Koushik Dutta
66a0ea08ec server: support binding to interfaces 2023-04-25 14:04:50 -07:00
Koushik Dutta
0527baf14a webrtc: update werift, remove unnecessary disable ipv6 option. addresses can be filtered individually. 2023-04-25 13:37:16 -07:00
Koushik Dutta
c7c5c6eed5 server: electron app hooks 2023-04-25 13:34:14 -07:00
Koushik Dutta
143c950c19 core: add support for multiple local addresses 2023-04-25 13:28:00 -07:00
Koushik Dutta
8d0bb0fa97 prebeta 2023-04-24 23:26:53 -07:00
Koushik Dutta
964274e50c prebeta 2023-04-24 23:22:32 -07:00
Koushik Dutta
e9844528aa python-codecs: add timestamps 2023-04-24 18:32:43 -07:00
Koushik Dutta
0609fc8986 python-codecs: publish typings fix 2023-04-24 11:46:14 -07:00
Koushik Dutta
9331b71433 opencv/sdk: fix typing.Union missing 2023-04-24 09:26:21 -07:00
Koushik Dutta
21f8239db7 videoanalysis: publish 2023-04-24 09:26:03 -07:00
Koushik Dutta
86042ec3fe sdk/videoanalysis: add zone hints to detection generator 2023-04-23 21:25:39 -07:00
Koushik Dutta
cdb87fb268 dummy-switch: further settings tweaks 2023-04-22 21:57:15 -07:00
Koushik Dutta
63dcd35b17 dummy-switch: friendly names on extensions 2023-04-22 21:54:35 -07:00
Koushik Dutta
951c3b9be6 dummy-switch: add replace binary sensor extension 2023-04-22 21:52:06 -07:00
Koushik Dutta
ed642bb3fe homekit: dont sync notifier toggle buttons by default 2023-04-22 21:35:07 -07:00
Koushik Dutta
8093cdd3d9 homekit: remove linked motion sensor 2023-04-22 21:29:12 -07:00
Koushik Dutta
fcbfc3a73f Merge branch 'main' of github.com:koush/scrypted 2023-04-22 21:27:54 -07:00
Koushik Dutta
94945a48bd dummy-switch: create replace motion sensor extension 2023-04-22 21:27:48 -07:00
Brett Jia
e360ede5cb rebroadcast: prebuffer on charging battery (#751)
* rework battery prebuffer to take into account charger interface

* rename handler

* do not restart exited stream on low battery

* tweak battery prebuffer state + periodically poll battery prebuffer state
2023-04-22 16:54:15 -07:00
Roarrk
bc9ec73567 coreml: accomodate MultiArray (Float32 0 × 80) models (#749)
Hack to accomodate models that has an output of type Float32 instead of Double.
2023-04-22 16:54:02 -07:00
Sheng
cd7e570508 chromecast: fix stop casting issue (#753) 2023-04-22 16:53:42 -07:00
Koushik Dutta
1b06c9c11d videoanalyis: pause motion detection while motion is active and resume after timeout 2023-04-22 10:10:46 -07:00
Koushik Dutta
154ab42d15 videonalaysis: refactor to avoid holding onto generators 2023-04-22 08:16:34 -07:00
Koushik Dutta
1929f6e8ed python-codecs: simplify generator code 2023-04-21 09:20:04 -07:00
Koushik Dutta
58bfa17cfe postrelease 2023-04-20 21:55:22 -07:00
Koushik Dutta
38c7006055 server: fix runaway cluster sockets 2023-04-20 21:55:15 -07:00
Koushik Dutta
b5e16b45a9 python-codecs: fix potential leak 2023-04-20 20:05:17 -07:00
Koushik Dutta
9c13668812 doorbird: publish 2023-04-20 11:58:10 -07:00
Koushik Dutta
a1ca724d6b opencv: support reference frame interval setting 2023-04-20 11:57:48 -07:00
Koushik Dutta
1b032d669c postrelease 2023-04-19 21:37:44 -07:00
Koushik Dutta
c492c15081 rpc: async generator should throw if yielded and when the peer has been killed. garbage collection does not trigger async generator return or throw. 2023-04-19 21:35:46 -07:00
Koushik Dutta
ee7076384b prebeta 2023-04-19 21:17:59 -07:00
Koushik Dutta
717cac721a detect: connect to rpc object for every videoframe 2023-04-19 12:18:02 -07:00
Koushik Dutta
af41c853bc Merge branch 'main' of github.com:koush/scrypted 2023-04-19 12:17:27 -07:00
Koushik Dutta
109b716753 sdk: update 2023-04-19 12:16:56 -07:00
Qasim Mehmood
07930508fe Publish mutable docker tags for all variants (#738)
This should add mutable docker tags for all variants that allow for updating via docker pull
2023-04-19 12:12:29 -07:00
nanosonde
a291abe375 Initial version of Doorbird plugin (#736)
save work

Add audio-transmit part

Fetch VGA JPEG snapshots from the camera

save work

Use fixed doorbird module 2.1.2

save work

Add doorbell and motion events

Clean up.

Improved initial camera setup like amcrest plugin

Update README
2023-04-19 12:12:18 -07:00
Koushik Dutta
f4f34b2da8 server: fix script 2023-04-18 10:47:32 -07:00
Koushik Dutta
3b4de526ba postrelease 2023-04-18 10:45:28 -07:00
Koushik Dutta
5de67fca86 server: fix python 3.8 issues 2023-04-18 10:45:20 -07:00
Koushik Dutta
98dc0b1b6d postrelease 2023-04-18 10:44:57 -07:00
Koushik Dutta
a05595ecc7 pam-diff/videoanalysis: fix performance, remove sharp dependency 2023-04-18 00:26:29 -07:00
Koushik Dutta
87be4648f1 prebeta 2023-04-17 22:14:36 -07:00
Koushik Dutta
60e51adb41 postrelease 2023-04-17 14:24:33 -07:00
Koushik Dutta
ace7720fe1 videoanalysis: fix snapshot hangs caused by HOL jpeg 2023-04-17 12:52:46 -07:00
Koushik Dutta
b9eb74d403 videoanalysis: add prebuffer hint 2023-04-17 09:55:20 -07:00
Koushik Dutta
fb7353383d predict: rollback rpc change until server is published 2023-04-17 08:46:32 -07:00
Koushik Dutta
bee119b486 python-codecs: handle vips rgba conversion 2023-04-17 08:46:24 -07:00
Koushik Dutta
0b6ffc2b87 predict: strip out allow list 2023-04-16 20:50:49 -07:00
Koushik Dutta
3863527b4d server: fix publish scripts 2023-04-16 13:11:44 -07:00
Koushik Dutta
51c48f4a1c prebeta 2023-04-16 13:10:41 -07:00
Koushik Dutta
4c138e9b4c prebeta 2023-04-16 12:11:25 -07:00
Koushik Dutta
e762c305a3 server: implement various python apis 2023-04-16 12:10:20 -07:00
Koushik Dutta
5bce335288 server: implement various python apis 2023-04-16 12:08:48 -07:00
Koushik Dutta
8201e9883a sdk: update python sdk 2023-04-16 11:43:02 -07:00
Koushik Dutta
74e5884285 videoanalysis: fix generator leak 2023-04-16 10:13:31 -07:00
Koushik Dutta
9cffd9ffbe server: fix noop cluster connect 2023-04-15 21:35:36 -07:00
Koushik Dutta
d8b617f2ae prebeta 2023-04-15 21:35:07 -07:00
Koushik Dutta
aeb564aa5d python-codecs: fix rgb->grasycale conversion 2023-04-15 21:22:52 -07:00
Koushik Dutta
45f672883a sdk: improve moving object metadata 2023-04-15 20:14:05 -07:00
Koushik Dutta
c0ff857a1b server: improve cluster resolution 2023-04-15 20:13:43 -07:00
Koushik Dutta
64f7e31f54 prebeta 2023-04-15 20:11:40 -07:00
Koushik Dutta
6b55f8876e prebeta 2023-04-15 15:12:10 -07:00
Koushik Dutta
718a31f2c5 prebeta 2023-04-15 15:02:32 -07:00
Koushik Dutta
c1e1d50fa5 sdk: publish 2023-04-15 10:14:42 -07:00
Koushik Dutta
75c4a1939f server: publish beta 2023-04-15 09:33:23 -07:00
Koushik Dutta
0d703c2aff predict: remove filter options 2023-04-15 09:33:10 -07:00
Koushik Dutta
0a6e4fda75 sdk: add support for designating object is moving 2023-04-14 22:29:05 -07:00
Koushik Dutta
4c2de9e443 server: add getDevice convenience method for pluginId/nativeId 2023-04-14 22:24:12 -07:00
Koushik Dutta
b8a4fedf1a client: publish 2023-04-14 22:08:58 -07:00
Koushik Dutta
79d9f1d4a1 server: add getDevice convenience method for pluginId/nativeId 2023-04-14 22:07:49 -07:00
Koushik Dutta
983213c578 sort-tracker: deprecate 2023-04-14 16:06:06 -07:00
Koushik Dutta
7dd3d71ebd videoanalysis: remove problematic ffmpeg video generator 2023-04-14 13:36:56 -07:00
Koushik Dutta
493f8deeef Revert "server: watch for dangling python processes"
This reverts commit b29f2d5ee1.
2023-04-14 13:02:51 -07:00
Koushik Dutta
b29f2d5ee1 server: watch for dangling python processes 2023-04-14 12:52:16 -07:00
Koushik Dutta
96bda10123 sort-tracker: remove average area check 2023-04-14 09:58:55 -07:00
Koushik Dutta
3294700d31 core: fix ui refresh issue 2023-04-14 08:07:12 -07:00
Koushik Dutta
0cf77d4c76 core: add support for date./time settings 2023-04-13 19:43:20 -07:00
Koushik Dutta
953841e3a5 update samples 2023-04-13 19:40:02 -07:00
Koushik Dutta
393c1017df sdk: add date/time/datetime types 2023-04-13 13:05:44 -07:00
Koushik Dutta
f50176d14a server: fix CPU usage being lost on fork exit 2023-04-13 12:53:21 -07:00
Koushik Dutta
7f2bf0b542 webrtc: fix ffmpeg leak 2023-04-13 12:51:52 -07:00
Koushik Dutta
9e3990400c zwave: publish 2023-04-13 12:51:41 -07:00
Koushik Dutta
95eed80735 webrtc: fix ffmpeg leak 2023-04-13 12:51:29 -07:00
Koushik Dutta
be43d0c017 zwave: publish 2023-04-12 09:47:05 -07:00
mikeburgh
386ea9a98a Fixing sensor mapping to position (#719) 2023-04-11 20:11:46 -07:00
Koushik Dutta
9b40978f61 client/server: fix various async generator bugs in remote client 2023-04-11 13:53:38 -07:00
Koushik Dutta
f0ee435cd0 videoanalysis: fix detection calculation/throttling in snapshot mode 2023-04-10 17:49:33 -07:00
Koushik Dutta
30748784ef videoanalysis: fix logging 2023-04-10 14:30:14 -07:00
Koushik Dutta
8310e33719 videoanalysis: profile system performance and use snapshot mode when necessary 2023-04-10 12:48:32 -07:00
Koushik Dutta
1d18697161 videoanalysis: watch for pipeline hangs. fix race conditions around pipeline startup/termination. 2023-04-10 10:17:26 -07:00
Koushik Dutta
d500b3fd6c h264 packetizer: update codec information with stapa packets 2023-04-10 08:06:41 -07:00
Koushik Dutta
95ae916b6c Merge branch 'main' of github.com:koush/scrypted 2023-04-09 21:02:47 -07:00
Koushik Dutta
ec3e16f20f onvif (reolink): implement two way audio 2023-04-09 21:02:42 -07:00
Brett Jia
30d28f543c arlo: boolean settings + publish (#713) 2023-04-09 14:49:43 -07:00
Koushik Dutta
e0cce24999 python-codecs: publish 2023-04-09 12:04:40 -07:00
Koushik Dutta
409b25f8b0 python-codecs: fix windows process cleanup 2023-04-09 12:04:16 -07:00
Koushik Dutta
8f278abec8 videoanalysis: fix bug where stream failure may cause motion detector to never restart 2023-04-08 11:10:49 -07:00
Koushik Dutta
d6179dab82 prebeta 2023-04-08 10:19:11 -07:00
Koushik Dutta
ed186e2142 server/rpc: improve typings on rpc message type 2023-04-08 10:19:06 -07:00
Koushik Dutta
3c021bb2c8 prebeta 2023-04-08 10:17:24 -07:00
Koushik Dutta
c522edc622 server/rpc: improve typings on rpc message type 2023-04-08 10:17:18 -07:00
Koushik Dutta
022a103bcb prebeta 2023-04-08 10:05:07 -07:00
Koushik Dutta
efd125b6e4 server/rpc: add Uint8Array to node transport safe arguments 2023-04-08 10:05:02 -07:00
Koushik Dutta
19f7688a65 python-codecs: publish 2023-04-08 09:18:54 -07:00
Koushik Dutta
7f18e4629c prebeta 2023-04-08 09:18:38 -07:00
Koushik Dutta
dfe2c937a1 server: add hook for cluster peer creation 2023-04-08 09:18:31 -07:00
Koushik Dutta
47d7a23a3d postrelease 2023-04-07 21:15:04 -07:00
Koushik Dutta
0ea609c80c server: update dependencies 2023-04-07 21:14:58 -07:00
Koushik Dutta
71ee5727f1 sdk: update 2023-04-07 15:11:24 -07:00
Koushik Dutta
2383f16112 videoanalysis: improve logging for debugging 2023-04-07 14:53:27 -07:00
Koushik Dutta
7d5defd736 homekit: publish 2023-04-07 14:06:38 -07:00
Koushik Dutta
cbf4cf0579 server: fix typo in storage of undefined 2023-04-07 14:06:17 -07:00
Koushik Dutta
422dd94e5c plugins: update problematic setStorage(key, undefined) 2023-04-07 14:05:49 -07:00
Koushik Dutta
076f5e27f1 postrelease 2023-04-07 13:50:09 -07:00
Koushik Dutta
645de2e5fd predict: improve input aspect ratio match fast path 2023-04-07 10:17:05 -07:00
Koushik Dutta
dcf24a77d7 postrelease 2023-04-07 08:30:27 -07:00
Koushik Dutta
7065365a47 postrelease 2023-04-07 08:30:21 -07:00
Koushik Dutta
b82520776e sdk/server: search for TypedDict in typing and typing_extensions 2023-04-07 08:29:47 -07:00
Koushik Dutta
638c1f77fd ring: fix login issues 2023-04-07 08:06:16 -07:00
Koushik Dutta
73a489ea37 rtc: null check double offer error 2023-04-06 22:35:52 -07:00
Koushik Dutta
77d69f025a server: fix release build scripts 2023-04-06 08:32:44 -07:00
Koushik Dutta
3bc14ad248 prebeta 2023-04-06 08:32:33 -07:00
Koushik Dutta
03e5a9dec1 Merge branch 'main' of github.com:koush/scrypted 2023-04-06 08:22:11 -07:00
Koushik Dutta
57b790c332 server: publish beta 2023-04-06 08:22:05 -07:00
Koushik Dutta
ce2ea63be7 server: add hook for npm exec in non-node environment (electron) 2023-04-06 08:21:37 -07:00
Alex Leeds
2dd4721b7f ring: fix login 406 error (#698) 2023-04-06 07:45:33 -07:00
Justin Angevaare
667075dfad Add tip about digest authentication (#697) 2023-04-06 07:45:24 -07:00
Koushik Dutta
7abdb06b66 postrelease 2023-04-05 14:39:28 -07:00
Koushik Dutta
43e5822c93 server: fix first run account creation bug 2023-04-05 14:39:20 -07:00
Koushik Dutta
bc579514e7 python-codecs: add numpy to requirements.txt 2023-04-05 11:55:04 -07:00
Koushik Dutta
825100f94e webrtc: add answer only option 2023-04-05 10:17:17 -07:00
Koushik Dutta
803bfc1560 pam-diff: tweak default motion percent 2023-04-05 10:16:46 -07:00
Koushik Dutta
b2013a54ed pam-diff: tweak default motion percent 2023-04-05 10:15:40 -07:00
Koushik Dutta
f252407935 rebroadcast: fix settings clear issue 2023-04-04 11:37:37 -07:00
Koushik Dutta
516f2a2a7b server: fetch version from package registry 2023-04-04 10:14:31 -07:00
Koushik Dutta
c1677ce691 postrelease 2023-04-04 09:59:39 -07:00
Koushik Dutta
5028fb812d server: storage polyfill should serialize keys and values as strings 2023-04-04 09:58:51 -07:00
Koushik Dutta
2db4e2579f server: add more files to .npmignore 2023-04-04 08:24:23 -07:00
Koushik Dutta
b339ca6cd2 fix bug where deleted users have continued/escalated permissions 2023-04-04 08:17:44 -07:00
Koushik Dutta
f100999cb1 postrelease 2023-04-04 08:17:13 -07:00
Koushik Dutta
2863756bd6 Revert "webrtc: startRtpForwarderProcess remove werift dependency"
This reverts commit 143a0b2c41.
2023-04-03 14:26:56 -07:00
Koushik Dutta
cc408850a0 videoanalysis: changing pipeline should restart video analysis 2023-04-03 12:26:02 -07:00
Koushik Dutta
ed1ceeda51 core: return correct acls for admins 2023-04-03 11:16:14 -07:00
Koushik Dutta
df09d8e92a Merge branch 'main' of github.com:koush/scrypted 2023-04-03 08:34:35 -07:00
Koushik Dutta
298ac960d1 core: fix checkbox ui 2023-04-03 08:34:30 -07:00
Nick Berardi
62d4d55aae unifi: added native zoom capability (#684) 2023-04-03 08:24:56 -07:00
Nick Berardi
a2121c0dc5 alexa: add setting to publish debug events to console (#685) 2023-04-03 08:24:31 -07:00
Koushik Dutta
9b5ea27c0b core: fix checkbox ui 2023-04-03 08:23:20 -07:00
Koushik Dutta
0b0e90fc04 server: fix version being off by 1 in release notes/tag 2023-04-02 10:13:52 -07:00
Koushik Dutta
d8aff609bf core: publish 2023-04-02 09:37:54 -07:00
Koushik Dutta
d8283c261a homekit: publish beta 2023-04-02 09:37:49 -07:00
Koushik Dutta
e3aca964be Merge branch 'main' of github.com:koush/scrypted 2023-04-02 09:34:07 -07:00
Koushik Dutta
a96025c45f prerelease 2023-04-02 09:33:56 -07:00
Koushik Dutta
6afd4b4579 server: aggressively kill python plugin processes and forks 2023-04-02 09:33:48 -07:00
Brett Jia
f97669949d sdk, core: add Charger interface (#680)
* add Charger interface

* add charger icon to web ui

* import correct path

* get charge state displayed correctly
2023-04-01 21:03:54 -07:00
Koushik Dutta
0a0a31574f Merge branch 'main' of github.com:koush/scrypted 2023-04-01 15:02:46 -07:00
Koushik Dutta
90fb751a22 reolink: stream spec hints 2023-04-01 15:02:39 -07:00
Brett Jia
b8d06fada5 arlo: toggle for wired power + audio sensors (#679)
* remove hints to force prebuffer snapshots to fetch stream

* cleanup exception guard + catch prebuffer snapshot errors

* settings to remove Battery interface

* delayed init to load battery percentage

* fix plugin crash due to missing smart features dict

* properly add toggle for wired power

* fix race condition when multiple settings are updated at once

* bump 0.7.10 for beta

* audio detection + clean up battery/no-battery settings

* bump 0.7.11 for beta

* remove basestation models from camera class

* bump 0.7.12 for release
2023-04-01 14:28:54 -07:00
Koushik Dutta
2cecb1686f core: fix ui hang, readd launcher 2023-03-31 23:21:10 -07:00
Koushik Dutta
db03775530 prerelease 2023-03-31 20:37:25 -07:00
Koushik Dutta
cccbc33f1a server: detect 32/64 mixed mode issue and provide hint on how to fix. https://github.com/koush/scrypted/issues/678 2023-03-31 20:37:14 -07:00
Koushik Dutta
5f23873366 videoanalysis: fix bug where motion sensor would stop on invalid condition 2023-03-31 12:37:52 -07:00
Koushik Dutta
e43accae67 Merge branch 'main' of github.com:koush/scrypted 2023-03-31 09:45:02 -07:00
Koushik Dutta
b3a0cda6f9 python-codecs: fix vips/yuv/gray fast path 2023-03-31 09:44:57 -07:00
Alex Leeds
58c3348282 hap: merge in sirens as child devices (#674)
* hap: merge in sirens as child devices

* add subtype to onOff base
2023-03-31 07:44:38 -07:00
Koushik Dutta
a9e6d76e99 python-codecs: fix libav jpeg export 2023-03-30 23:59:05 -07:00
Koushik Dutta
3b58936387 predict: remove dead code 2023-03-30 09:35:47 -07:00
Koushik Dutta
3a14ab81c8 sample: update 2023-03-30 09:35:37 -07:00
Koushik Dutta
291178a7b5 sdk/client: update 2023-03-30 09:34:57 -07:00
Koushik Dutta
b65faf1a79 opencv: add gray toBuffer fast path 2023-03-30 09:34:45 -07:00
Koushik Dutta
9d8a1353c0 opencv: fix motion box translation 2023-03-29 17:25:24 -07:00
Koushik Dutta
b29d793178 ring: remove accidental clearing of clips cache 2023-03-29 16:42:09 -07:00
Koushik Dutta
d8e406d415 webrtc: reduce debug logging 2023-03-29 16:41:16 -07:00
Koushik Dutta
4529872fd6 videoanalysis: make sharp optional 2023-03-29 14:03:35 -07:00
Koushik Dutta
fa86c31340 prerelease 2023-03-29 12:41:56 -07:00
Koushik Dutta
94ded75d40 docker: fix watchtower token 2023-03-29 12:17:05 -07:00
Koushik Dutta
887b61cd7a prebeta 2023-03-29 11:58:54 -07:00
Koushik Dutta
48e3d30987 server: output docker flavor to logs 2023-03-29 11:58:43 -07:00
Koushik Dutta
02dba3cd71 docker: include flavor in env variable 2023-03-29 11:57:11 -07:00
Koushik Dutta
195769034d docker: include flavor in env variable 2023-03-29 11:56:50 -07:00
Koushik Dutta
39c08aa378 prebeta 2023-03-29 10:19:18 -07:00
Koushik Dutta
fa8056d38e python: purge packages on update 2023-03-29 10:18:34 -07:00
Koushik Dutta
145f116c68 webrtc/h264: reset stapa sent flag after every idr frame 2023-03-29 09:37:41 -07:00
Koushik Dutta
15b6f336e4 common: add h264 fragment information parsing 2023-03-29 08:18:13 -07:00
Koushik Dutta
8b46f0a466 openv: use new pipieline 2023-03-29 08:17:52 -07:00
Koushik Dutta
a20cc5cd89 docker: always install packages for arm 2023-03-29 08:01:08 -07:00
Koushik Dutta
3d068929fd predict: publish 2023-03-28 19:40:14 -07:00
Koushik Dutta
928f9b7579 prerelease 2023-03-28 19:36:48 -07:00
Koushik Dutta
c1c5a42645 server: fixup versioned prefix/node_modules path 2023-03-28 19:36:39 -07:00
Koushik Dutta
12643cdde2 Merge branch 'main' of github.com:koush/scrypted 2023-03-28 19:27:26 -07:00
Koushik Dutta
0bff96a6e6 python-codecs: pil crop is not thread safe https://github.com/python-pillow/Pillow/issues/4848 2023-03-28 19:27:22 -07:00
TA2k
4e7e67de54 Enable ipv6 for avahi (#670)
Enable ipv6 for avahi to allow multiple mdns server on one host
2023-03-28 13:32:14 -07:00
Koushik Dutta
65c4a30004 rebroadcast: use regular file open flags for truncate 2023-03-28 12:45:52 -07:00
Koushik Dutta
309a1dc11f rebroadcast: add truncation error logging 2023-03-28 12:43:07 -07:00
Koushik Dutta
b7904b73b2 Merge branch 'main' of github.com:koush/scrypted 2023-03-28 12:20:14 -07:00
Koushik Dutta
9e9ddbc5f3 rebroadcast: catch various unhandled errors 2023-03-28 12:20:07 -07:00
Koushik Dutta
ceda54f91b rebroadcast: support recording truncation 2023-03-28 12:19:38 -07:00
Koushik Dutta
1d4052b839 common: simplify some socket utils 2023-03-28 10:26:48 -07:00
Koushik Dutta
6a5d6e6617 predict: cleanups 2023-03-28 10:26:43 -07:00
Koushik Dutta
f55cc6066f common: simplify some socket utils 2023-03-28 10:25:50 -07:00
Brett Jia
527714e434 arlo: camera [spot,flood]lights, sirens + only use interfaces when hardware supports it (#660)
* only create vss and siren for supported basestation models

* VideoClips only if camera has cloud recording + start of Cameras as DeviceProviders

* make verbose logging a boolean toggle

* camera spotlights and floodlights

* tweak video clip delete warning

* bump 0.7.5 for beta

* bump 0.7.6 for release + pin deps

* expose sirens on supported cameras

* bump 0.7.7 for release
2023-03-27 16:43:23 -07:00
Koushik Dutta
8a1633ffa3 tensorflow: reduce dependencies for new pipeline 2023-03-27 12:23:44 -07:00
Koushik Dutta
56b2ab9c4f prerelease 2023-03-27 11:53:24 -07:00
Koushik Dutta
d330e2eb9d server: remove os machine usage which only exists in recent node builds 2023-03-27 11:53:19 -07:00
Koushik Dutta
b55e7cacb3 predict: remove old pipline code 2023-03-27 11:14:53 -07:00
Koushik Dutta
c70375db06 prerelease 2023-03-27 09:37:39 -07:00
Koushik Dutta
2c23021d40 server: catch/print startup errors to console and not just events tab 2023-03-27 09:37:29 -07:00
Koushik Dutta
84a4ef4539 mac: reorder unpin 2023-03-27 09:02:37 -07:00
Koushik Dutta
7f3db0549b python-codecs: update requirements.txt 2023-03-27 08:52:20 -07:00
Koushik Dutta
de0e1784a3 amcrest: fix camera default name 2023-03-27 08:50:01 -07:00
Koushik Dutta
5a8798638e homekit: do not start two way audio if only an rtcp packet is received 2023-03-27 08:48:40 -07:00
Koushik Dutta
14da49728c videoanalysis: remove old pipeline 2023-03-26 23:28:52 -07:00
Koushik Dutta
55423b2d09 videoanalysis: yuv/gray extraction fixes 2023-03-26 23:03:08 -07:00
Koushik Dutta
596106247b python-codecs: fix libav and pil issues 2023-03-26 22:43:13 -07:00
Koushik Dutta
5472d90368 opencv: beta 2023-03-26 19:21:22 -07:00
Koushik Dutta
fcf58413fc prebeta 2023-03-26 12:25:30 -07:00
Koushik Dutta
0d03b91753 server: add query tokens to env auth 2023-03-26 12:25:23 -07:00
Koushik Dutta
2fd088e4d6 prebeta 2023-03-26 12:09:21 -07:00
Koushik Dutta
c6933198b2 server: autocreate admin if specified by env 2023-03-26 12:09:15 -07:00
Koushik Dutta
210e684a22 docker: fix watchtower scope https://github.com/koush/scrypted/issues/662 2023-03-26 11:38:38 -07:00
Koushik Dutta
53cc4b6ef3 python-codecs: fix older version of pil 2023-03-26 11:36:09 -07:00
Koushik Dutta
d58d138a68 mac: trim deps, unpin hacked up gst libs 2023-03-25 22:03:14 -07:00
Koushik Dutta
c0199a2b76 mac: remove gstreamer hack from install script 2023-03-25 21:55:57 -07:00
Koushik Dutta
badb1905ce prerelease 2023-03-25 21:54:40 -07:00
Koushik Dutta
735c2dce7b Merge branch 'main' of github.com:koush/scrypted 2023-03-25 21:52:56 -07:00
Koushik Dutta
ffae3f246f python-codecs: fix mac crash 2023-03-25 21:52:51 -07:00
Koushik Dutta
31b424f89f server: mac python fixes 2023-03-25 21:52:32 -07:00
Brett Jia
3b7acc3a90 homekit: merge child lights into cameras (#659) 2023-03-25 20:09:42 -07:00
Koushik Dutta
7e66d1ac7f prebeta 2023-03-25 19:45:11 -07:00
Koushik Dutta
a613da069e server: relax failure on python arch mismatch 2023-03-25 19:45:05 -07:00
Koushik Dutta
40b73c6589 prebeta 2023-03-25 18:42:52 -07:00
Koushik Dutta
ef16ca83a2 server: detect python architecture vs machine mismatch 2023-03-25 18:42:39 -07:00
Koushik Dutta
76bf1d0d3f docker: rollback linux changes 2023-03-25 18:35:40 -07:00
Koushik Dutta
3d5ccf25d1 server: log host os specs 2023-03-25 15:05:08 -07:00
Koushik Dutta
36fcb713d9 videoanalysis: ffmpeg frame generator fixes 2023-03-25 15:04:40 -07:00
Koushik Dutta
e306631850 docker: arm fixes 2023-03-25 14:40:37 -07:00
Koushik Dutta
17400fa886 docker: arm fixes 2023-03-25 14:37:17 -07:00
Koushik Dutta
c6dc628616 docker: arm fixes 2023-03-25 14:31:40 -07:00
Koushik Dutta
f974653e73 videoanalysis: make new pipeline the default 2023-03-25 12:05:35 -07:00
Koushik Dutta
b83880a8a3 Merge branch 'main' of github.com:koush/scrypted 2023-03-25 11:34:37 -07:00
Koushik Dutta
ee4d8f52df pam-diff: fixup score reporting 2023-03-25 11:34:33 -07:00
Brett Jia
3854b75c6e arlo: video clips + virtual security system for sirens (#656)
* fix doorbell device type

* bump 0.7.1 for beta

* standalone camera fixes

* bump 0.7.2 for beta

* more type annotations + trickle discover all devices

* fetch arlo library clips

* log options

* cache library at lower level and fetch clips on demand

* move library timedelta range lower in stack

* wip siren as security system

* virtual security system and tweaks

* vss documentation and settings

* expand vss usage docs

* more docs changes

* force homekit and scrypted to update given vss and siren state

* RE-ENABLING SIREN!!!

* bump 0.7.3 for beta

* bump 0.7.3 for release
2023-03-25 11:13:28 -07:00
Koushik Dutta
07c3173506 docker: fix pip execution command 2023-03-25 10:43:12 -07:00
Koushik Dutta
2894ab1b96 prerelease 2023-03-25 09:28:26 -07:00
Koushik Dutta
99995ea882 server: start watchdog/stats after plugin dependency installation completes 2023-03-25 09:27:04 -07:00
Koushik Dutta
d6560fbbe4 prerelease 2023-03-24 21:50:42 -07:00
Koushik Dutta
7205583104 prebeta 2023-03-24 20:45:10 -07:00
Koushik Dutta
8479a16d3d vscode-typescript: update sample 2023-03-24 20:21:11 -07:00
Koushik Dutta
409aad4794 videoanalysis: publish 2023-03-24 20:20:22 -07:00
Koushik Dutta
a29d009e5c sdk: update 2023-03-24 20:19:48 -07:00
Koushik Dutta
6772419ccf rpc: async iterator close should happen silently 2023-03-24 20:19:41 -07:00
Koushik Dutta
38746ee743 homekit: publish beta 2023-03-24 19:52:39 -07:00
Brett Jia
c5cb3ffa90 homekit: merge SecuritySystem & child Sirens as one Homekit accessory (#650)
* add OnOff to SecuritySystem accessory as a switch

* BFS to reorder devices + merge and skip devices

* add some comments

* more safety checks

* embarrassing moment where I forgot this isn't BFS
2023-03-24 19:52:10 -07:00
Brett Jia
e119056267 actions: tests for platform-specific install scripts (#653)
* Create test.yml

* debugging

* Update install-scrypted-dependencies-linux.sh

* Update install-scrypted-dependencies-linux.sh

* Update test.yml

* Update install-scrypted-dependencies-linux.sh

* Update test.yml

* Update test.yml

* Update test.yml

* Run for only certain paths
2023-03-24 08:23:08 -07:00
Koushik Dutta
590ad3de37 Merge branch 'main' of github.com:koush/scrypted 2023-03-23 19:59:47 -07:00
Koushik Dutta
6cd412de88 detect: add format hints 2023-03-23 19:59:26 -07:00
Koushik Dutta
33ca0242b1 videoanalysis: dps tracker 2023-03-23 19:42:17 -07:00
Koushik Dutta
68d3f10888 rtsp server: fix URL quirk in electron 2023-03-23 19:42:00 -07:00
Koushik Dutta
7a844aac84 sdk: format hints on video frame 2023-03-23 19:41:45 -07:00
Koushik Dutta
6f2bb9fd9e server: hack fix python sdk path 2023-03-23 00:37:17 -07:00
Koushik Dutta
12e47993a4 server: electron environment fixes 2023-03-22 22:50:02 -07:00
Koushik Dutta
b0396b77bd server: serialization intrinsic for Uint8Array 2023-03-22 22:49:28 -07:00
Koushik Dutta
07c2314376 server: update package lock 2023-03-22 19:52:06 -07:00
Koushik Dutta
cee140e49f sort-tracker: publish 2023-03-22 19:51:52 -07:00
Koushik Dutta
a3963af6e7 prebeta 2023-03-22 14:47:32 -07:00
Koushik Dutta
8ff28418b3 mac: install cmake 2023-03-22 14:13:56 -07:00
Koushik Dutta
08a5c2f2b3 docker: set SCRYPTED_BASE_VERSION 2023-03-22 14:00:22 -07:00
Koushik Dutta
286bd5b19e docker: set SCRYPTED_BASE_VERSION 2023-03-22 13:58:01 -07:00
Koushik Dutta
59f3c2e3ad server: add mechanism to force pip/npm install rerun on upgrade 2023-03-22 13:54:04 -07:00
Koushik Dutta
ea1b394061 tensorflow-lite: publish 2023-03-22 13:27:25 -07:00
Koushik Dutta
5dc1af76e8 Merge branch 'main' of github.com:koush/scrypted 2023-03-22 13:15:42 -07:00
Koushik Dutta
771bbd834b python-codecs: use pillow-simd if possible 2023-03-22 13:15:35 -07:00
Alex Leeds
418724f860 ring: fix video clip thumbnails (#651) 2023-03-22 13:12:42 -07:00
Koushik Dutta
2ecf48bc60 python-codecs: add Pillow fallback 2023-03-22 13:08:32 -07:00
Koushik Dutta
d19b942d2c videoanalysis: remove kernel monkey patch 2023-03-21 23:14:33 -07:00
Koushik Dutta
08e724759d videoanalysis: use deterministic defaultsg 2023-03-21 23:06:34 -07:00
Koushik Dutta
80031bc80b videoanalysis: publish 2023-03-21 23:00:45 -07:00
Koushik Dutta
beb53c672c videoanalysis: include ffmpeg frame grabber 2023-03-21 23:00:08 -07:00
Koushik Dutta
0dc75bf737 prebeta 2023-03-21 21:23:51 -07:00
Koushik Dutta
59008fb964 server: fix bug where python async generator aclose is not called on rpc objects 2023-03-21 21:16:08 -07:00
Koushik Dutta
b119e5ee00 videoanalsyis: log when image is saved as jpeg 2023-03-21 16:22:40 -07:00
Koushik Dutta
01d0f4c72a pam-diff: new pipeline support 2023-03-21 16:17:41 -07:00
Koushik Dutta
9fe3f1a4db docker: add libjpeg-dev 2023-03-21 08:57:30 -07:00
Koushik Dutta
60bf112ebd reolnk: remove hikvision default name 2023-03-21 08:56:43 -07:00
Koushik Dutta
45aa443889 homekit: beta 2023-03-21 08:55:14 -07:00
Koushik Dutta
08f4922860 docker: add libjpeg-dev 2023-03-21 08:52:39 -07:00
Koushik Dutta
899970405a sdk: fix literals and unions 2023-03-20 23:22:46 -07:00
Koushik Dutta
b4a3960e43 Merge branch 'main' of github.com:koush/scrypted 2023-03-20 23:20:24 -07:00
Koushik Dutta
0514e62d78 mqtt: make it fork compatible 2023-03-20 23:20:19 -07:00
Alex Leeds
3621e58d4c hap: remove persistence from camera type (#649) 2023-03-20 20:51:55 -07:00
Koushik Dutta
506b24026f docker: Fix armv7 check 2023-03-20 20:22:44 -07:00
Koushik Dutta
98b67f5d56 docker: Fix armv7l detection 2023-03-20 19:21:27 -07:00
Koushik Dutta
33c95aa0e8 prebeta 2023-03-20 17:18:45 -07:00
Koushik Dutta
7d8f86bb6c sdk: dont add sourcemap if none was generated 2023-03-20 17:18:36 -07:00
Koushik Dutta
d6717cc58b server: add additional plugin runtime hooks for electron 2023-03-20 17:18:18 -07:00
Koushik Dutta
673f8e3b2a docker: trim dependencies further 2023-03-20 17:13:03 -07:00
Koushik Dutta
cae87ba414 docker: reorder 2023-03-20 16:35:55 -07:00
Koushik Dutta
13362fd53e docker: remove ffmpeg, use node static binary, and remove gstreamer ugly plugins 2023-03-20 16:35:01 -07:00
Koushik Dutta
d9f2ba0665 docker: further reduce dependencies 2023-03-20 16:31:24 -07:00
Koushik Dutta
64a0f90a9a docker: reduce dependencies for non arm7, pil will have wheels 2023-03-20 16:19:26 -07:00
Koushik Dutta
88300910a2 detect: clean up requirements 2023-03-20 16:18:36 -07:00
Koushik Dutta
7face43d54 python-codecs: fix decoder selection getting clobbered 2023-03-20 15:50:07 -07:00
Koushik Dutta
6a9f35ce2a docker: undo typing change 2023-03-20 14:07:52 -07:00
Koushik Dutta
effe76f251 docker: update massive nvidia build 2023-03-20 13:30:10 -07:00
Koushik Dutta
58d5539cb8 Merge branch 'main' of github.com:koush/scrypted 2023-03-20 13:29:33 -07:00
Koushik Dutta
d956ee06d0 tensorflow: update to new api, add opencv dep 2023-03-20 13:29:28 -07:00
Alex Leeds
8ddf91d13b ring: fix location mode subscriptions (#646)
* ring: fix location mode subscriptions

* Update package-lock.json

* ring: probe devices after discovery
2023-03-20 11:28:50 -07:00
Koushik Dutta
3f65cd4f6d Update Dockerfile.nvidia 2023-03-20 08:43:50 -07:00
Koushik Dutta
3ffdbf9d2b Update Dockerfile.nvidia 2023-03-20 08:43:20 -07:00
Koushik Dutta
a51754b0e3 docker: add tensorflow nvidia support 2023-03-20 08:42:28 -07:00
slyoldfox
e8ee21e567 btcino 0.0.7 / sip 0.0.6 (#644)
* * Fix an issues in SIP.js where the ACK and BYE replies didn't go to the correct uri

* * Implemented outgoing SIP MESSAGE sending
* Adding voice mail check
* Adding a lock for a bticino doorbell

* Cleanup dependencies, code in sip, bticino plugins

* Cleanup dependencies, code in sip, bticino plugins

* Clear stale devices from our map and clear the voicemail check

* Do not require register() for a SIP call

* Narrow down the event matching to deletes of devices

* Use releaseDevice to clean up stale entries

* Fix uuid version

* Attempt to make two way audio work

* Attempt to make two way audio work - fine tuning

* Enable incoming doorbell events

* SipCall was never a "sip call" but more like a manager
SipSession was more the "sip call"

* * Rename sip registered session to persistent sip manager
* Allow handling of call pickup in homekit (hopefully!)

* * use the consoles from the camera object

* * use the consoles from the camera object

* * Fix the retry timer

* * Added webhook url

* * parse record route correctly

* * Add gruu and use a custom fork of sip.js which supports keepAlive SIP clients (and dropped Websocket)
* use cross-env in package.json

* Added webhook urls for faster handling of events

* Added videoclips

* plugins/sip 0.0.6

* plugins/bticino 0.0.7
2023-03-20 07:19:08 -07:00
Koushik Dutta
420f070035 prebeta 2023-03-19 18:27:25 -07:00
Koushik Dutta
c78cbc04d3 server: add non invoking entry point 2023-03-19 18:27:21 -07:00
Koushik Dutta
dddf565fbe prebeta 2023-03-19 18:26:42 -07:00
Koushik Dutta
0516ca810d server: add non invoking entry point 2023-03-19 18:26:28 -07:00
Alex Leeds
fac67696a9 ring: add lights, switches & outlets (#642) 2023-03-19 14:59:14 -07:00
Koushik Dutta
c62d4bd3fd prebeta 2023-03-19 14:53:32 -07:00
Koushik Dutta
877e1d4992 server: export start promise to wait for https server 2023-03-19 14:53:26 -07:00
Koushik Dutta
35b5cddd95 Merge branch 'main' of github.com:koush/scrypted 2023-03-19 14:46:51 -07:00
Koushik Dutta
a86fb128d9 prebeta 2023-03-19 14:46:42 -07:00
Koushik Dutta
983daae971 server: expose gc programatically 2023-03-19 14:46:37 -07:00
Koushik Dutta
9b687e3286 prebeta 2023-03-19 14:41:16 -07:00
Koushik Dutta
abfd0ffe35 server: add explicit hook for main file 2023-03-19 14:41:02 -07:00
Alex Leeds
407afa1d8c ring: add glass break & mailbox sensor (#641)
* ring: add glass break sensor

* ring: add beams motion sensor
2023-03-19 08:55:56 -07:00
Koushik Dutta
9bafe97ef6 sdk: updatre 2023-03-18 22:12:09 -07:00
Koushik Dutta
cb151e79d8 Merge branch 'main' of github.com:koush/scrypted 2023-03-18 20:43:33 -07:00
Koushik Dutta
7e6230d7b0 onvif: make relative movement the ptz default 2023-03-18 20:43:23 -07:00
Alex Leeds
7d95de389a ring: split location and camera from main file (#640) 2023-03-18 20:18:56 -07:00
Koushik Dutta
2ce187bc98 snapshot: readd timeout that got lost during vips revert 2023-03-18 17:11:20 -07:00
Koushik Dutta
100671265e opencv: fix frame import from buffer in new pipeline 2023-03-18 17:00:14 -07:00
Koushik Dutta
965d5af631 Merge remote-tracking branch 'origin/hap' 2023-03-18 16:59:43 -07:00
Koushik Dutta
a19f356a66 server: start python plugins outside of exception handling if gi is unavailable 2023-03-18 14:25:27 -07:00
Alex Leeds
a520357a23 hap: fix camera init without sensor (#639) 2023-03-18 14:01:34 -07:00
Koushik Dutta
d92d130a7c Merge branch 'main' of github.com:koush/scrypted 2023-03-18 13:57:48 -07:00
Koushik Dutta
c8dd7d2f04 mac: include libav codecs 2023-03-18 13:57:42 -07:00
Koushik Dutta
b85b589675 detect: VAAPI crash fix, avoid decodebin usage if possible 2023-03-18 13:57:24 -07:00
Alex Leeds
9b4cbed28f ring: restructure plugin (#638) 2023-03-18 13:03:08 -07:00
Brett Jia
6b1794d32f remote: add reconnect and disable WIP mediaobject converter (#637) 2023-03-18 11:19:48 -07:00
Koushik Dutta
aefe4b6849 Merge branch 'main' of github.com:koush/scrypted 2023-03-18 10:40:22 -07:00
Koushik Dutta
a68395174a python-codecs: add missing band extraction named argument 2023-03-18 10:40:16 -07:00
Brett Jia
8a56e789b7 arlo: add basestation and basestation siren + other tweaks (#636)
* configure stream refresh

* use modelId directly

* bump 0.6.8

* lower refresh rate to 20 min

* basestations as DeviceProviders + various type annotations

* reorder

* trickle discover basestations to avoid clobbering cameras

* generalize device creation + start of siren

* functional basestation siren

* bump 0.7.0 for beta
2023-03-18 10:26:02 -07:00
Koushik Dutta
06ef146c5b Merge remote-tracking branch 'origin/hap' 2023-03-18 09:20:55 -07:00
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
Alex Leeds
21eeab6c3c hap: fix characteristic warnings (#621)
* hap: fix characteristic warnings

* hap: add avahi support through docker compose and add docs
2023-03-16 10:39:52 -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
18c6edd310 homekit: beta 2023-03-09 08:13:37 -08:00
Koushik Dutta
a1d7a0d9ca homekit: fixup hap recordign cleanup 2023-03-08 15:34:17 -08:00
Koushik Dutta
5d5078534d homekit: fixup hap implementation 2023-03-08 15:08:22 -08:00
Koushik Dutta
537a968e2e Merge remote-tracking branch 'alex/hap' into hap 2023-03-08 13:41:49 -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
Alex Leeds
4520d1d29f homekit: update hap 2023-03-08 13:07:59 -05: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
427 changed files with 29601 additions and 24516 deletions

View File

@@ -69,12 +69,18 @@ jobs:
tags: |
${{ format('koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '' && 'koush/scrypted:full' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'koush/scrypted:lite' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'koush/scrypted:thin' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:lite-s6' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '.s6' && 'koush/scrypted:thin-s6' || '' }}
${{ format('ghcr.io/koush/scrypted:{0}{1}-v{2}', matrix.BASE, matrix.SUPERVISOR, github.event.inputs.package_version || steps.package-version.outputs.current-version) }}
${{ matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '.s6' && format('ghcr.io/koush/scrypted:{0}', github.event.inputs.docker_tag) || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-full' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:full' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:lite' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '' && 'ghcr.io/koush/scrypted:thin' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-lite' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:lite-s6' || '' }}
${{ github.event.inputs.docker_tag == 'latest' && matrix.BASE == '18-bullseye-thin' && matrix.SUPERVISOR == '.s6' && 'ghcr.io/koush/scrypted:thin-s6' || '' }}
cache-from: type=gha
cache-to: type=gha,mode=max

60
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Test
on:
push:
branches: ["main"]
paths: ["docker/**", ".github/workflows/test.yml"]
pull_request:
paths: ["docker/**", ".github/workflows/test.yml"]
workflow_dispatch:
jobs:
test_linux_local:
name: Test Linux local installation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run install script
run: |
cat ./docker/install-scrypted-dependencies-linux.sh | sudo SERVICE_USER=$USER bash
- name: Test server is running
run: |
systemctl status scrypted.service
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
test_mac_local:
name: Test Mac local installation
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run install script
run: |
mkdir -p ~/.scrypted
bash ./docker/install-scrypted-dependencies-mac.sh
- name: Test server is running
run: |
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/
test_windows_local:
name: Test Windows local installation
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run install script
run: |
.\docker\install-scrypted-dependencies-win.ps1
- name: Test server is running
run: |
curl -k --retry 20 --retry-all-errors --retry-max-time 600 https://localhost:10443/

13
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "plugins/homekit/HAP-NodeJS"]
path = external/HAP-NodeJS
url = ../../koush/HAP-NodeJS
[submodule "plugins/unifi-protect/src/unifi-protect"]
path = external/unifi-protect
url = ../../koush/unifi-protect.git
@@ -19,6 +16,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,21 +26,12 @@
[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
url = ../../koush/sort_oh.git
[submodule "plugins/cloud/node-nat-upnp"]
path = plugins/cloud/node-nat-upnp
url = ../../koush/node-nat-upnp.git

View File

@@ -1,15 +1,17 @@
export class Deferred<T> {
finished = false;
resolve!: (value: T|PromiseLike<T>) => void;
reject!: (error: Error) => void;
resolve!: (value: T|PromiseLike<T>) => this;
reject!: (error: Error) => this;
promise: Promise<T> = new Promise((resolve, reject) => {
this.resolve = v => {
this.finished = true;
resolve(v);
return this;
};
this.reject = e => {
this.finished = true;
reject(e);
return this;
};
});
}

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

@@ -361,8 +361,7 @@ export interface RebroadcasterOptions {
},
}
export async function handleRebroadcasterClient(duplex: Promise<Duplex> | Duplex, options?: RebroadcasterOptions) {
const socket = await duplex;
export function handleRebroadcasterClient(socket: Duplex, options?: RebroadcasterOptions) {
const firstWriteData = (data: StreamChunk) => {
if (data.startStream) {
socket.write(data.startStream)

View File

@@ -62,4 +62,4 @@ export async function bind(server: dgram.Socket, port: number) {
}
}
export { listenZero, listenZeroSingleClient } from "@scrypted/server/src/listen-zero";
export { listenZero, listenZeroSingleClient, ListenZeroSingleClientTimeoutError } from "@scrypted/server/src/listen-zero";

View File

@@ -250,7 +250,8 @@ export class BrowserSignalingSession implements RTCSignalingSession {
function logSendCandidate(console: Console, type: string, session: RTCSignalingSession): RTCSignalingSendIceCandidate {
return async (candidate) => {
try {
console.log(`${type} trickled candidate:`, candidate.sdpMLineIndex, candidate.candidate);
if (localStorage.getItem('debugLog') === 'true')
console.log(`${type} trickled candidate:`, candidate.sdpMLineIndex, candidate.candidate);
await session.addIceCandidate(candidate);
}
catch (e) {
@@ -297,7 +298,7 @@ export async function connectRTCSignalingClients(
if (offerOptions?.offer && answerOptions?.offer)
throw new Error('Both RTC clients have offers and can not negotiate. Consider implementing this in @scrypted/webrtc.');
if (offerOptions?.requiresOffer && answerOptions.requiresOffer)
if (offerOptions?.requiresOffer && answerOptions?.requiresOffer)
throw new Error('Both RTC clients require offers and can not negotiate.');
offerSetup.type = 'offer';
@@ -308,11 +309,13 @@ export async function connectRTCSignalingClients(
const offer = await offerClient.createLocalDescription('offer', offerSetup as RTCAVSignalingSetup,
disableTrickle ? undefined : answerQueue.queueSendCandidate);
console.log('offer sdp', offer.sdp);
if (localStorage.getItem('debugLog') === 'true')
console.log('offer sdp', offer.sdp);
await answerClient.setRemoteDescription(offer, answerSetup as RTCAVSignalingSetup);
const answer = await answerClient.createLocalDescription('answer', answerSetup as RTCAVSignalingSetup,
disableTrickle ? undefined : offerQueue.queueSendCandidate);
console.log('answer sdp', answer.sdp);
if (localStorage.getItem('debugLog') === 'true')
console.log('answer sdp', answer.sdp);
await offerClient.setRemoteDescription(answer, offerSetup as RTCAVSignalingSetup);
offerQueue.flush();
answerQueue.flush();

View File

@@ -4,15 +4,16 @@ 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';
import { sleep } from './sleep';
import { StreamChunk, StreamParser, StreamParserOptions } from './stream-parser';
import { URL } from 'url';
const REQUIRED_WWW_AUTHENTICATE_KEYS = ['realm', 'nonce'];
@@ -47,6 +48,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;
@@ -105,6 +129,16 @@ export function getNaluTypes(streamChunk: StreamChunk) {
return getNaluTypesInNalu(streamChunk.chunks[streamChunk.chunks.length - 1].subarray(12))
}
export function getNaluFragmentInformation(nalu: Buffer) {
const naluType = nalu[0] & 0x1f;
const fua = naluType === H264_NAL_TYPE_FU_A;
return {
fua,
fuaStart: fua && !!(nalu[1] & 0x80),
fuaEnd: fua && !!(nalu[1] & 0x40),
}
}
export function getNaluTypesInNalu(nalu: Buffer, fuaRequireStart = false, fuaRequireEnd = false) {
const ret = new Set<number>();
const naluType = nalu[0] & 0x1f;
@@ -284,18 +318,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[]> {
@@ -568,7 +591,7 @@ export class RtspClient extends RtspBase {
const username = decodeURIComponent(authedUrl.username);
const password = decodeURIComponent(authedUrl.password);
const strippedUrl = new URL(url);
const strippedUrl = new URL(url.toString());
strippedUrl.username = '';
strippedUrl.password = '';
@@ -590,9 +613,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> {
@@ -660,7 +681,7 @@ export class RtspClient extends RtspBase {
});
}
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions) {
async setup(options: RtspClientTcpSetupOptions | RtspClientUdpSetupOptions, headers?: Headers) {
const protocol = options.type === 'udp' ? '' : '/TCP';
const client = options.type === 'udp' ? 'client_port' : 'interleaved';
let port: number;
@@ -676,9 +697,9 @@ export class RtspClient extends RtspBase {
port = options.dgram.address().port;
options.dgram.on('message', data => options.onRtp(undefined, data));
}
const headers: any = {
headers = Object.assign({
Transport: `RTP/AVP${protocol};unicast;${client}=${port}-${port + 1}`,
};
}, headers);
const response = await this.request('SETUP', headers, options.path);
let interleaved: {
begin: number;
@@ -1053,3 +1074,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

@@ -7,7 +7,9 @@
# install script.
################################################################
ARG BUILDPACK_DEPS_BASE="bullseye"
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
FROM debian:${BUILDPACK_DEPS_BASE} as header
RUN apt-get update && apt-get -y install curl wget
# switch to nvm?
ARG NODE_VERSION=18
@@ -22,6 +24,13 @@ RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update
RUN apt-get -y install libedgetpu1-std
# intel opencl gpu for openvino
RUN if [ "$(uname -m)" = "x86_64" ]; \
then \
apt-get -y install \
intel-opencl-icd; \
fi
RUN apt-get -y install software-properties-common apt-utils
RUN apt-get -y update
RUN apt-get -y upgrade
@@ -31,40 +40,52 @@ 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 \
libvips \
pkg-config
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN apt-get -y install \
libjpeg-dev zlib1g-dev
# 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 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \
python3 \
python3-dev \
python3-gi \
python3-gst-1.0 \
python3-matplotlib \
python3-numpy \
python3-opencv \
python3-pil \
python3-pip \
python3-setuptools \
python3-skimage \
python3-wheel
# armv7l does not have wheels for any of these
# and compile times would forever, if it works at all.
# furthermore, it's possible to run 32bit docker on 64bit arm,
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
RUN if [ "$(uname -m)" != "x86_64" ]; \
then \
apt-get -y install \
python3-matplotlib \
python3-numpy \
python3-opencv \
python3-pil \
python3-skimage; \
fi
# python pip
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
RUN python3 -m pip install dlib
# 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 python3 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
################################################################
# End section generated from template/Dockerfile.full.header
@@ -79,6 +100,11 @@ ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=full
################################################################
# End section generated from template/Dockerfile.full.footer
################################################################

View File

@@ -1,5 +1,7 @@
ARG BUILDPACK_DEPS_BASE="bullseye"
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
FROM debian:${BUILDPACK_DEPS_BASE} as header
RUN apt-get update && apt-get -y install curl wget
# switch to nvm?
ARG NODE_VERSION=18
@@ -15,26 +17,30 @@ RUN apt-get -y update
# base development stuff
RUN apt-get -y install \
build-essential \
cmake \
gcc \
libcairo2-dev \
libgirepository1.0-dev \
libglib2.0-dev \
pkg-config
# python native
RUN apt-get -y install \
python3 \
python3-dev \
python3-gi \
python3-pip \
python3-setuptools \
python3-wheel
# python pip
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
ENV SCRYPTED_DOCKER_SERVE="true"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=lite

22
docker/Dockerfile.nvidia Normal file
View File

@@ -0,0 +1,22 @@
FROM koush/18-bullseye-full.s6
WORKDIR /
# Install miniconda
ENV CONDA_DIR /opt/conda
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh && \
/bin/bash ~/miniconda.sh -b -p /opt/conda
# Put conda in path so we can use conda activate
ENV PATH=$CONDA_DIR/bin:$PATH
RUN conda install -c conda-forge cudatoolkit=11.2.2 cudnn=8.1.0
ENV CONDA_PREFIX=/opt/conda
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_PREFIX/lib/
# this is a copy pasta, seems to need a reinstall.
# 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 python3 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil

View File

@@ -1,5 +1,7 @@
ARG BUILDPACK_DEPS_BASE="bullseye"
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
FROM debian:${BUILDPACK_DEPS_BASE} as header
RUN apt-get update && apt-get -y install curl wget
# switch to nvm?
ARG NODE_VERSION=18
@@ -12,14 +14,12 @@ RUN apt-get -y upgrade
RUN apt-get -y install software-properties-common apt-utils
RUN apt-get -y update
# base development stuff
RUN apt-get -y install \
build-essential \
gcc \
libglib2.0-dev \
pkg-config
ENV SCRYPTED_DOCKER_SERVE="true"
ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=thin

3
docker/docker-build-nvidia.sh Executable file
View File

@@ -0,0 +1,3 @@
./docker-build.sh
docker build -t koush/scrypted:18-bullseye-full.nvidia -f Dockerfile.nvidia

View File

@@ -32,12 +32,14 @@ services:
restart: unless-stopped
network_mode: host
# uncomment this and a line below as needed.
# devices:
# zwave usb serial device
# - /dev/ttyACM0:/dev/ttyACM0
# all usb devices, such as coral tpu
# - /dev/bus/usb:/dev/bus/usb
devices:
# hardware accelerated video decoding, opencl, etc.
- /dev/dri:/dev/dri
# uncomment below as necessary.
# zwave usb serial device
# - /dev/ttyACM0:/dev/ttyACM0
# all usb devices, such as coral tpu
# - /dev/bus/usb:/dev/bus/usb
volumes:
- ~/.scrypted/volume:/server/volume
@@ -52,6 +54,11 @@ services:
# target: /nvr
# volume:
# nocopy: true
# uncomment the following lines to expose Avahi, an mDNS advertiser.
# make sure Avahi is running on the host machine, otherwise this will not work.
# - /var/run/dbus:/var/run/dbus
# - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket
# logging is noisy and will unnecessarily wear on flash storage.
# scrypted has per device in memory logging that is preferred.
logging:
@@ -83,4 +90,4 @@ services:
# Must match the port in the auto update url above.
- 10444:8080
# check for updates once an hour (interval is in seconds)
command: --interval 3600 --cleanup
command: --interval 3600 --cleanup --scope scrypted

View File

@@ -1,7 +1,7 @@
[server]
#host-name=
use-ipv4=yes
use-ipv6=no
use-ipv6=yes
enable-dbus=yes
ratelimit-interval-usec=1000000
ratelimit-burst=1000
@@ -14,4 +14,4 @@ rlimit-core=0
rlimit-data=4194304
rlimit-fsize=0
rlimit-nofile=768
rlimit-stack=4194304
rlimit-stack=4194304

View File

@@ -40,48 +40,29 @@ 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
# 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
RUN_IGNORE brew install pillow
# snapshot plugin and others
RUN brew install libvips
# dlib
RUN brew install cmake
### HACK WORKAROUND
### https://github.com/koush/scrypted/issues/544
brew unpin gstreamer
brew unpin gst-python
brew unpin gst-plugins-ugly
brew unpin gst-plugins-good
brew unpin gst-plugins-base
brew unpin gst-plugins-good
brew unpin gst-plugins-bad
brew unlink gstreamer
brew unlink gst-python
brew unlink gst-plugins-ugly
brew unlink gst-plugins-good
brew unlink gst-plugins-base
brew unlink gst-plugins-bad
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gstreamer.rb && brew install ./gstreamer.rb
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-python.rb && brew install ./gst-python.rb
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-ugly.rb && brew install ./gst-plugins-ugly.rb
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-good.rb && brew install ./gst-plugins-good.rb
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-base.rb && brew install ./gst-plugins-base.rb
curl -O https://raw.githubusercontent.com/Homebrew/homebrew-core/49a8667f0c1a6579fe887bc0fa1c0ce682eb01c8/Formula/gst-plugins-bad.rb && brew install ./gst-plugins-bad.rb
brew pin gstreamer
brew pin gst-python
brew pin gst-plugins-ugly
brew pin gst-plugins-good
brew pin gst-plugins-base
brew pin gst-plugins-bad
brew unpin gst-plugins-ugly
brew unpin gst-libav
brew unpin gst-python
### END HACK WORKAROUND
# gstreamer plugins
RUN_IGNORE brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-libav
# gst python bindings
RUN_IGNORE brew install gst-python
ARCH=$(arch)
if [ "$ARCH" = "arm64" ]
then
@@ -102,7 +83,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

@@ -42,7 +42,7 @@ fi
WATCHTOWER_HTTP_API_TOKEN=$(echo $RANDOM | md5sum)
DOCKER_COMPOSE_YML=$SCRYPTED_HOME/docker-compose.yml
echo "Created $DOCKER_COMPOSE_YML"
curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum)"/g > $DOCKER_COMPOSE_YML
curl -s https://raw.githubusercontent.com/koush/scrypted/main/docker/docker-compose.yml | sed s/SET_THIS_TO_SOME_RANDOM_TEXT/"$(echo $RANDOM | md5sum | head -c 32)"/g > $DOCKER_COMPOSE_YML
echo "Setting permissions on $SCRYPTED_HOME"
chown -R $SERVICE_USER $SCRYPTED_HOME

View File

@@ -8,6 +8,11 @@ ENV SCRYPTED_CAN_RESTART="true"
ENV SCRYPTED_VOLUME="/server/volume"
ENV SCRYPTED_INSTALL_PATH="/server"
# changing this forces pip and npm to perform reinstalls.
# if this base image changes, this version must be updated.
ENV SCRYPTED_BASE_VERSION=20230329
ENV SCRYPTED_DOCKER_FLAVOR=full
################################################################
# End section generated from template/Dockerfile.full.footer
################################################################

View File

@@ -4,7 +4,9 @@
# install script.
################################################################
ARG BUILDPACK_DEPS_BASE="bullseye"
FROM buildpack-deps:${BUILDPACK_DEPS_BASE} as header
FROM debian:${BUILDPACK_DEPS_BASE} as header
RUN apt-get update && apt-get -y install curl wget
# switch to nvm?
ARG NODE_VERSION=18
@@ -19,6 +21,13 @@ RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
RUN apt-get -y update
RUN apt-get -y install libedgetpu1-std
# intel opencl gpu for openvino
RUN if [ "$(uname -m)" = "x86_64" ]; \
then \
apt-get -y install \
intel-opencl-icd; \
fi
RUN apt-get -y install software-properties-common apt-utils
RUN apt-get -y update
RUN apt-get -y upgrade
@@ -26,40 +35,54 @@ 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 \
libvips \
pkg-config
# these are necessary for pillow-simd, additional on disk size is small
# but could consider removing this.
RUN apt-get -y install \
libjpeg-dev zlib1g-dev
# 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 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-alsa \
gstreamer1.0-vaapi
# python native
RUN apt-get -y install \
python3 \
python3-dev \
python3-gi \
python3-gst-1.0 \
python3-matplotlib \
python3-numpy \
python3-opencv \
python3-pil \
python3-pip \
python3-setuptools \
python3-skimage \
python3-wheel
# armv7l does not have wheels for any of these
# and compile times would forever, if it works at all.
# furthermore, it's possible to run 32bit docker on 64bit arm,
# which causes weird behavior in python which looks at the arch version
# which still reports 64bit, even if running in 32bit docker.
# this scenario is not supported and will be reported at runtime.
RUN if [ "$(uname -m)" != "x86_64" ]; \
then \
apt-get -y install \
python3-matplotlib \
python3-numpy \
python3-opencv \
python3-pil \
python3-skimage; \
fi
# python pip
RUN python3 -m pip install --upgrade pip
RUN python3 -m pip install aiofiles debugpy typing_extensions typing psutil
# 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 python3 -m pip install --force-reinstall --no-binary :all: cffi
RUN python3 -m pip install aiofiles debugpy typing_extensions psutil
################################################################
# End section generated from template/Dockerfile.full.header

1
external/HAP-NodeJS vendored

Submodule external/HAP-NodeJS deleted from 3fe1f920f5

Submodule external/push-receiver deleted from d054e083d6

View File

@@ -27,13 +27,6 @@ echo "sdk > npm run build"
npm run build
popd
pushd external/HAP-NodeJS
echo "external/HAP-NodeJS > npm install"
npm install
echo "external/HAP-NodeJS > npm run build"
npm run build
popd
pushd external/werift
echo "external/werift > npm install"
npm install

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,58 +1,29 @@
{
"name": "@scrypted/client",
"version": "1.1.38",
"lockfileVersion": 2,
"version": "1.1.51",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/client",
"version": "1.1.38",
"version": "1.1.51",
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"@scrypted/types": "^0.2.80",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"engine.io-client": "^6.4.0",
"rimraf": "^3.0.2"
},
"devDependencies": {
"@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.80",
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.80.tgz",
"integrity": "sha512-YVu7jcD5sYgjJLP7kH1K2FJzqrlcjdpDxzZoLXudZCKiujldbmLYcwglSgnN9bRqkKZcGOfru/WssvQj+0JioQ=="
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
@@ -69,9 +40,9 @@
}
},
"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/axios": {
@@ -99,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"
},
@@ -118,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",
@@ -159,17 +130,17 @@
"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"
},
@@ -183,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"
@@ -213,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"
}
@@ -221,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"
}
@@ -241,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",
@@ -256,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"
},
@@ -286,177 +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/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
},
"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.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=="
},
"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.38",
"version": "1.1.51",
"description": "",
"main": "dist/packages/client/src/index.js",
"scripts": {
@@ -13,13 +13,13 @@
"license": "ISC",
"devDependencies": {
"@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",
"@scrypted/types": "^0.2.80",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"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';
@@ -7,6 +7,7 @@ import { timeoutPromise } from "../../../common/src/promise-utils";
import { BrowserSignalingSession, waitPeerConnectionIceConnected, waitPeerIceConnectionClosed } from "../../../common/src/rtc-signaling";
import { DataChannelDebouncer } from "../../../plugins/webrtc/src/datachannel-debouncer";
import type { IOSocket } from '../../../server/src/io';
import { MediaObject } from '../../../server/src/plugin/mediaobject';
import type { MediaObjectRemote } from '../../../server/src/plugin/plugin-api';
import { attachPluginRemote } from '../../../server/src/plugin/plugin-remote';
import { RpcPeer } from '../../../server/src/rpc';
@@ -49,6 +50,7 @@ export interface ScryptedClientStatic extends ScryptedStatic {
connectionType: ScryptedClientConnectionType;
authorization?: string;
queryToken?: { [parameter: string]: string };
rpcPeer: RpcPeer,
}
export interface ScryptedConnectionOptions {
@@ -503,23 +505,8 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
} = scrypted;
console.log('api attached', Date.now() - start);
mediaManager.createMediaObject = async (data, mimeType, options) => {
const mo: MediaObjectRemote & {
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: any,
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
} = {
[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION]: true,
[RpcPeer.PROPERTY_PROXY_PROPERTIES]: {
mimeType,
sourceId: options?.sourceId,
},
mimeType,
sourceId: options?.sourceId,
async getData() {
return data;
},
};
return mo;
mediaManager.createMediaObject = async<T extends MediaObjectOptions>(data: any, mimeType: string, options: T) => {
return new MediaObject(mimeType, data, options) as any;
}
const { browserSignalingSession, connectionManagementId, updateSessionId } = rpcPeer.params;
@@ -587,6 +574,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
browserSignalingSession,
authorization,
queryToken,
rpcPeer,
}
socket.on('close', () => {

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.4",
"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,26 @@
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;
function debug(...args: any[]) {
if (DEBUG)
console.debug(...args);
}
class AlexaPlugin extends ScryptedDeviceBase implements HttpRequestHandler, MixinProvider, Settings {
storageSettings = new StorageSettings(this, {
tokenInfo: {
hide: true,
@@ -22,96 +30,202 @@ 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.',
type: 'string',
readonly: true
},
debug: {
title: 'Debug Events',
description: 'Log all events to the console. This will be very noisy and should not be left enabled.',
type: 'boolean',
onPut(oldValue: boolean, newValue: boolean) {
DEBUG = newValue;
}
}
});
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);
DEBUG = this.storageSettings.values.debug ?? false;
this.syncDevices();
alexaHandlers.set('Alexa.Authorization/AcceptGrant', this.onAlexaAuthorization);
alexaHandlers.set('Alexa.Discovery/Discover', this.onDiscoverEndpoints);
systemManager.listen(async (eventSource, eventDetails, eventData) => {
if (!eventSource)
return;
this.start();
}
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
return;
async start() {
const supportedType = supportedTypes.get(eventSource.type);
if (!supportedType) {
this.console.warn(`${eventSource.name} no longer supported type?`);
return;
for (const id of Object.keys(systemManager.getSystemState())) {
const device = systemManager.getDeviceById(id);
await this.tryEnableMixin(device);
}
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;
let report = await supportedType.sendEvent(eventSource, eventDetails, eventData);
if (!report && eventDetails.eventInterface === ScryptedInterface.Online) {
report = {};
}
if (!report && eventDetails.eventInterface === ScryptedInterface.Battery) {
report = {};
}
if (!report) {
this.console.warn(`${eventDetails.eventInterface}.${eventDetails.property} not supported for device ${eventSource.type}`);
return;
}
debug("event", eventDetails.eventInterface, eventDetails.property, eventSource.type);
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 +260,8 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
const endpoint = await this.getAlexaEndpoint();
const self = this;
debug("send event to alexa", data);
return axios.post(`https://${endpoint}/v3/events`, data, {
headers: {
'Authorization': 'Bearer ' + accessToken,
@@ -160,25 +276,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 +338,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 +392,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 +468,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 +482,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 +499,7 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
},
"payload": {}
}
}));
});
} catch (error) {
this.console.error(`AcceptGrant.Response failed because ${error}`);
@@ -363,14 +511,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 +529,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 +556,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,79 +579,25 @@ 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 {
debug("making authorization request to Scrypted");
await axios.get('https://home.scrypted.app/_punch/getcookie', {
headers: {
'Authorization': authorization,
@@ -501,42 +608,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;
}
const mapName = `${namespace}/${name}`;
return capHandler.apply(this, [request, response, directive, device]);
debug("received directive from alexa", mapName, body);
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
debug("response to alexa directive", 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.121",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/amcrest",
"version": "0.0.115",
"version": "0.0.121",
"license": "Apache",
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
@@ -16,7 +16,7 @@
"multiparty": "^4.2.2"
},
"devDependencies": {
"@types/node": "^16.11.0"
"@types/node": "^18.15.11"
}
},
"../../common": {
@@ -36,7 +36,7 @@
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.87",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -100,9 +100,9 @@
}
},
"node_modules/@types/node": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"node_modules/auth-header": {
"version": "1.0.0",
@@ -291,9 +291,9 @@
}
},
"@types/node": {
"version": "16.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.0.tgz",
"integrity": "sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg=="
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"auth-header": {
"version": "1.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/amcrest",
"version": "0.0.115",
"version": "0.0.121",
"description": "Amcrest Plugin for Scrypted",
"author": "Scrypted",
"license": "Apache",
@@ -36,12 +36,12 @@
},
"dependencies": {
"@koush/axios-digest-auth": "^0.8.5",
"@scrypted/sdk": "file:../../sdk",
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/multiparty": "^0.0.33",
"multiparty": "^4.2.2"
},
"devDependencies": {
"@types/node": "^16.11.0"
"@types/node": "^18.15.11"
}
}

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,8 +605,18 @@ 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';
settings.newCamera ||= 'Amcrest Camera';
nativeId = await super.createDevice(settings, nativeId);
@@ -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

@@ -22,6 +22,6 @@
//"scrypted.volumeRoot": "${config:scrypted.serverRoot}/volume",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/scrypted_python"
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}

View File

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

View File

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

View File

@@ -27,9 +27,10 @@ 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
from datetime import datetime, timedelta
from cachetools import cached, TTLCache
import asyncio
import sys
@@ -227,7 +228,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 +379,36 @@ 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 SubscribeToAudioEvents(self, basestation, camera, callback):
"""
Use this method to subscribe to audio events. You must provide a callback function which will get called once per audio event.
The callback function should have the following signature:
def callback(self, event)
This is an example of handling a specific event, in reality, you'd probably want to write a callback for HandleEvents()
that has a big switch statement in it to handle all the various events Arlo produces.
Returns the Task object that contains the subscription loop.
"""
resource = f"cameras/{camera.get('deviceId')}"
def callbackwrapper(self, event):
properties = event.get('properties', {})
stop = None
if 'audioDetected' in properties:
stop = callback(properties['audioDetected'])
if not stop:
return None
return stop
return asyncio.get_event_loop().create_task(
self.HandleEvents(basestation, resource, [('is', 'audioDetected')], callbackwrapper)
)
def SubscribeToBatteryEvents(self, basestation, camera, callback):
"""
@@ -403,7 +433,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 +469,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 +490,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 +518,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 +540,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 +560,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 +652,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 +696,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 +726,176 @@ 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,
)
def SirenOn(self, basestation, camera=None):
if camera is not None:
resource = f"siren/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"sirenState": "on",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
return self.Notify(basestation, {
"action": "set",
"resource": "siren",
"publishResponse": True,
"properties": {
"sirenState": "on",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
def SirenOff(self, basestation, camera=None):
if camera is not None:
resource = f"siren/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"sirenState": "off",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
return self.Notify(basestation, {
"action": "set",
"resource": "siren",
"publishResponse": True,
"properties": {
"sirenState": "off",
"duration": 300,
"volume": 8,
"pattern": "alarm"
}
})
def SpotlightOn(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"spotlight": {
"enabled": True,
},
},
})
def SpotlightOff(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"spotlight": {
"enabled": False,
},
},
})
def FloodlightOn(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"floodlight": {
"on": True,
},
},
})
def FloodlightOff(self, basestation, camera):
resource = f"cameras/{camera.get('deviceId')}"
return self.Notify(basestation, {
"action": "set",
"resource": resource,
"publishResponse": True,
"properties": {
"floodlight": {
"on": False,
},
},
})
def GetLibrary(self, device, from_date: datetime, to_date: datetime):
"""
This call returns the following:
presignedContentUrl is a link to the actual video in Amazon AWS.
presignedThumbnailUrl is a link to the thumbnail .jpg of the actual video in Amazon AWS.
[
{
"mediaDurationSecond": 30,
"contentType": "video/mp4",
"name": "XXXXXXXXXXXXX",
"presignedContentUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX.mp4?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"lastModified": 1472881430181,
"localCreatedDate": XXXXXXXXXXXXX,
"presignedThumbnailUrl": "https://arlos3-prod-z2.s3.amazonaws.com/XXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXXX/XXX-XXXXXXX/XXXXXXXXXXXXX/recordings/XXXXXXXXXXXXX_thumb.jpg?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1472968703&Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"reason": "motionRecord",
"deviceId": "XXXXXXXXXXXXX",
"createdBy": "XXXXXXXXXXXXX",
"createdDate": "20160903",
"timeZone": "America/Chicago",
"ownerId": "XXX-XXXXXXX",
"utcCreatedDate": XXXXXXXXXXXXX,
"currentState": "new",
"mediaDuration": "00:00:30"
}
]
"""
# give the query range a bit of buffer
from_date_internal = from_date - timedelta(days=1)
to_date_internal = to_date + timedelta(days=1)
return [
result for result in
self._getLibraryCached(from_date_internal.strftime("%Y%m%d"), to_date_internal.strftime("%Y%m%d"))
if result["deviceId"] == device["deviceId"]
and datetime.fromtimestamp(int(result["name"]) / 1000.0) <= to_date
and datetime.fromtimestamp(int(result["name"]) / 1000.0) >= from_date
]
@cached(cache=TTLCache(maxsize=512, ttl=60))
def _getLibraryCached(self, from_date: str, to_date: str):
logger.debug(f"Library cache miss for {from_date}, {to_date}")
return self.request.post(
f'https://{self.BASE_URL}/hmsweb/users/library',
{
'dateFrom': from_date,
'dateTo': to_date
}
)
def GetSmartFeatures(self, device) -> dict:
smart_features = self._getSmartFeaturesCached()
key = f"{device['owner']['ownerId']}_{device['deviceId']}"
return smart_features["features"].get(key, {})
@cached(cache=TTLCache(maxsize=1, ttl=60))
def _getSmartFeaturesCached(self) -> dict:
return self.request.get(f'https://{self.BASE_URL}/hmsweb/users/subscription/smart/features')

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

@@ -0,0 +1,69 @@
from __future__ import annotations
import traceback
from typing import List, TYPE_CHECKING
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Device
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloDeviceBase(ScryptedDeviceBase, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
nativeId: str = None
arlo_device: dict = None
arlo_basestation: dict = None
provider: ArloProvider = None
stop_subscriptions: bool = False
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId)
self.logger_name = nativeId
self.nativeId = nativeId
self.arlo_device = arlo_device
self.arlo_basestation = arlo_basestation
self.provider = provider
self.logger.setLevel(self.provider.get_current_log_level())
def __del__(self) -> None:
self.stop_subscriptions = True
self.cancel_pending_tasks()
def get_applicable_interfaces(self) -> List[str]:
"""Returns the list of Scrypted interfaces that applies to this device."""
return []
def get_device_type(self) -> str:
"""Returns the Scrypted device type that applies to this device."""
return ""
def get_device_manifest(self) -> Device:
"""Returns the Scrypted device manifest representing this device."""
parent = None
if self.arlo_device.get("parentId") and self.arlo_device["parentId"] != self.arlo_device["deviceId"]:
parent = self.arlo_device["parentId"]
return {
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": self.arlo_device["deviceId"],
"name": self.arlo_device["deviceName"],
"interfaces": self.get_applicable_interfaces(),
"type": self.get_device_type(),
"providerNativeId": parent,
}
def get_builtin_child_device_manifests(self) -> List[Device]:
"""Returns the list of child device manifests representing hardware features built into this device."""
return []

View File

@@ -0,0 +1,71 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Device, DeviceProvider, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .vss import ArloSirenVirtualSecuritySystem
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloBasestation(ArloDeviceBase, DeviceProvider):
MODELS_WITH_SIRENS = [
"vmb4000",
"vmb4500"
]
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_basestation, arlo_basestation=arlo_basestation, provider=provider)
@property
def has_siren(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloBasestation.MODELS_WITH_SIRENS])
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.DeviceProvider.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.DeviceProvider.value
def get_builtin_child_device_manifests(self) -> List[Device]:
if not self.has_siren:
# this basestation has no builtin siren, so no manifests to return
return []
vss = self.get_or_create_vss()
return [
{
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": vss.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
"interfaces": vss.get_applicable_interfaces(),
"type": vss.get_device_type(),
"providerNativeId": self.nativeId,
},
] + vss.get_builtin_child_device_manifests()
async def getDevice(self, nativeId: str) -> ScryptedDeviceBase:
if not nativeId.startswith(self.nativeId):
# must be a camera, so get it from the provider
return await self.provider.getDevice(nativeId)
if not nativeId.endswith("vss"):
return None
return self.get_or_create_vss()
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
if not self.vss:
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.vss

View File

@@ -1,47 +1,88 @@
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import json
import threading
import time
from typing import List, TYPE_CHECKING
import scrypted_arlo_go
import scrypted_sdk
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Settings, Camera, VideoCamera, MotionSensor, Battery, ScryptedMimeTypes, ScryptedInterface
from scrypted_sdk.types import Setting, Settings, Device, Camera, VideoCamera, VideoClips, VideoClip, VideoClipOptions, MotionSensor, AudioSensor, Battery, DeviceProvider, MediaObject, ResponsePictureOptions, ResponseMediaStreamOptions, ScryptedMimeTypes, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .spotlight import ArloSpotlight, ArloFloodlight
from .vss import ArloSirenVirtualSecuritySystem
from .child_process import HeartbeatChildProcess
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin
from .util import BackgroundTaskMixin, async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor, Battery, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
timeout = 30
nativeId = None
arlo_device = None
arlo_basestation = None
provider = None
class ArloCamera(ArloDeviceBase, Settings, Camera, VideoCamera, DeviceProvider, VideoClips, MotionSensor, AudioSensor, Battery):
MODELS_WITH_SPOTLIGHTS = [
"vmc4040p",
"vmc2030",
"vmc2032",
"vmc4041p",
"vmc4050p",
"vmc5040",
"vml2030",
"vml4030",
]
def __init__(self, nativeId, arlo_device, arlo_basestation, provider):
super().__init__(nativeId=nativeId)
MODELS_WITH_FLOODLIGHTS = ["fb1001"]
self.logger_name = nativeId
MODELS_WITH_SIRENS = [
"vmc4040p",
"fb1001",
"vmc2030",
"vmc2020",
"vmc2032",
"vmc4041p",
"vmc4050p",
"vmc5040",
"vml2030",
"vmc4030",
"vml4030",
"vmc4030p",
]
self.nativeId = nativeId
self.arlo_device = arlo_device
self.arlo_basestation = arlo_basestation
self.provider = provider
self.logger.setLevel(self.provider.get_current_log_level())
MODELS_WITH_AUDIO_SENSORS = [
"vmc4040p",
"fb1001",
"vmc4041p",
"vmc4050p",
"vmc5040",
"vmc3040",
"vmc3040s",
"vmc4030",
"vml4030",
"vmc4030p",
]
self.intercom_session = None
MODELS_WITHOUT_BATTERY = [
"avd1001",
"vmc3040",
"vmc3040s",
]
self.stop_subscriptions = False
timeout: int = 30
intercom_session = None
light: ArloSpotlight = None
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.start_motion_subscription()
self.start_audio_subscription()
self.start_battery_subscription()
def __del__(self):
self.stop_subscriptions = True
self.cancel_pending_tasks()
def start_motion_subscription(self):
def start_motion_subscription(self) -> None:
def callback(motionDetected):
self.motionDetected = motionDetected
return self.stop_subscriptions
@@ -50,7 +91,22 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
self.provider.arlo.SubscribeToMotionEvents(self.arlo_basestation, self.arlo_device, callback)
)
def start_battery_subscription(self):
def start_audio_subscription(self) -> None:
if not self.has_audio_sensor:
return
def callback(audioDetected):
self.audioDetected = audioDetected
return self.stop_subscriptions
self.register_task(
self.provider.arlo.SubscribeToAudioEvents(self.arlo_basestation, self.arlo_device, callback)
)
def start_battery_subscription(self) -> None:
if self.wired_to_power:
return
def callback(batteryLevel):
self.batteryLevel = batteryLevel
return self.stop_subscriptions
@@ -59,12 +115,11 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
self.provider.arlo.SubscribeToBatteryEvents(self.arlo_basestation, self.arlo_device, callback)
)
def get_applicable_interfaces(self) -> list:
def get_applicable_interfaces(self) -> List[str]:
results = set([
ScryptedInterface.VideoCamera.value,
ScryptedInterface.Camera.value,
ScryptedInterface.MotionSensor.value,
ScryptedInterface.Battery.value,
ScryptedInterface.Settings.value,
])
@@ -76,21 +131,75 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
results.add(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)
if self.has_battery:
results.add(ScryptedInterface.Battery.value)
if self.wired_to_power:
results.discard(ScryptedInterface.Battery.value)
if self.has_siren or self.has_spotlight or self.has_floodlight:
results.add(ScryptedInterface.DeviceProvider.value)
if self.has_audio_sensor:
results.add(ScryptedInterface.AudioSensor.value)
if self.has_cloud_recording:
results.add(ScryptedInterface.VideoClips.value)
if not self._can_push_to_talk():
results.discard(ScryptedInterface.RTCSignalingChannel.value)
results.discard(ScryptedInterface.Intercom.value)
return list(results)
def get_device_type(self) -> str:
return ScryptedDeviceType.Camera.value
def get_builtin_child_device_manifests(self) -> List[Device]:
results = []
if self.has_spotlight or self.has_floodlight:
light = self.get_or_create_spotlight_or_floodlight()
results.append({
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": light.nativeId,
"name": f'{self.arlo_device["deviceName"]} {"Spotlight" if self.has_spotlight else "Floodlight"}',
"interfaces": light.get_applicable_interfaces(),
"type": light.get_device_type(),
"providerNativeId": self.nativeId,
})
if self.has_siren:
vss = self.get_or_create_vss()
results.extend([
{
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": vss.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren Virtual Security System',
"interfaces": vss.get_applicable_interfaces(),
"type": vss.get_device_type(),
"providerNativeId": self.nativeId,
},
] + vss.get_builtin_child_device_manifests())
return results
@property
def webrtc_emulation(self):
def webrtc_emulation(self) -> bool:
if self.storage:
return self.storage.getItem("webrtc_emulation")
return True if self.storage.getItem("webrtc_emulation") else False
else:
return False
@property
def two_way_audio(self):
def two_way_audio(self) -> bool:
if self.storage:
val = self.storage.getItem("two_way_audio")
if val is None:
@@ -99,9 +208,53 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
else:
return True
async def getSettings(self):
@property
def wired_to_power(self) -> bool:
if self.storage:
return True if self.storage.getItem("wired_to_power") else False
else:
return False
@property
def has_cloud_recording(self) -> bool:
return self.provider.arlo.GetSmartFeatures(self.arlo_device).get("planFeatures", {}).get("eventRecording", False)
@property
def has_spotlight(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SPOTLIGHTS])
@property
def has_floodlight(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_FLOODLIGHTS])
@property
def has_siren(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_SIRENS])
@property
def has_audio_sensor(self) -> bool:
return any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITH_AUDIO_SENSORS])
@property
def has_battery(self) -> bool:
return not any([self.arlo_device["modelId"].lower().startswith(model) for model in ArloCamera.MODELS_WITHOUT_BATTERY])
async def getSettings(self) -> List[Setting]:
result = []
if self.has_battery:
result.append(
{
"key": "wired_to_power",
"title": "Plugged In to External Power",
"value": self.wired_to_power,
"description": "Informs Scrypted that this device is plugged in to an external power source. " + \
"Will allow features like persistent prebuffer to work, however will no longer report this device's battery percentage. " + \
"Note that a persistent prebuffer may cause excess battery drain if the external power is not able to charge faster than the battery consumption rate.",
"type": "boolean",
},
)
if self._can_push_to_talk():
return [
result.extend([
{
"key": "two_way_audio",
"title": "(Experimental) Enable native two-way audio",
@@ -117,25 +270,31 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
"If enabled, takes precedence over native two-way audio. May use increased system resources.",
"type": "boolean",
},
]
])
return result
@async_print_exception_guard
async def putSetting(self, key, value) -> None:
if key in ["webrtc_emulation", "two_way_audio", "wired_to_power"]:
self.storage.setItem(key, value == "true" or value == True)
await self.provider.discover_devices()
async def getPictureOptions(self) -> List[ResponsePictureOptions]:
return []
async def putSetting(self, key, value):
if key in ["webrtc_emulation", "two_way_audio"]:
self.storage.setItem(key, value == "true")
await self.provider.discoverDevices()
async def getPictureOptions(self):
return []
async def takePicture(self, options=None):
@async_print_exception_guard
async def takePicture(self, options: dict = None) -> MediaObject:
self.logger.info("Taking picture")
real_device = await scrypted_sdk.systemManager.api.getDeviceById(self.getScryptedProperty("id"))
msos = await real_device.getVideoStreamOptions()
if any(["prebuffer" in m for m in msos]):
self.logger.info("Getting snapshot from prebuffer")
return await real_device.getVideoStream({"refresh": False})
try:
return await real_device.getVideoStream({"refresh": False})
except Exception as e:
self.logger.warning(f"Could not fetch from prebuffer due to: {e}")
self.logger.warning("Will try to fetch snapshot from Arlo cloud")
pic_url = await asyncio.wait_for(self.provider.arlo.TriggerFullFrameSnapshot(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
self.logger.debug(f"Got snapshot URL for at {pic_url}")
@@ -145,7 +304,7 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
return await scrypted_sdk.mediaManager.createMediaObject(str.encode(pic_url), ScryptedMimeTypes.Url.value)
async def getVideoStreamOptions(self):
async def getVideoStreamOptions(self) -> List[ResponseMediaStreamOptions]:
return [
{
"id": 'default',
@@ -163,59 +322,159 @@ class ArloCamera(ScryptedDeviceBase, Settings, Camera, VideoCamera, MotionSensor
}
]
async def _getVideoStreamURL(self):
async def _getVideoStreamURL(self) -> str:
self.logger.info("Requesting stream")
rtsp_url = await asyncio.wait_for(self.provider.arlo.StartStream(self.arlo_basestation, self.arlo_device), timeout=self.timeout)
self.logger.debug(f"Got stream URL at {rtsp_url}")
return rtsp_url
async def getVideoStream(self, options=None):
async def getVideoStream(self, options: dict = None) -> MediaObject:
self.logger.debug("Entered getVideoStream")
rtsp_url = await self._getVideoStreamURL()
return await scrypted_sdk.mediaManager.createMediaObject(str.encode(rtsp_url), ScryptedMimeTypes.Url.value)
mso = (await self.getVideoStreamOptions())[0]
mso['refreshAt'] = round(time.time() * 1000) + 30 * 60 * 1000
ffmpeg_input = {
'url': rtsp_url,
'container': 'rtsp',
'mediaStreamOptions': mso,
'inputArguments': [
'-f', 'rtsp',
'-i', rtsp_url,
]
}
return await scrypted_sdk.mediaManager.createFFmpegMediaObject(ffmpeg_input)
@async_print_exception_guard
async def startRTCSignalingSession(self, scrypted_session):
try:
plugin_session = ArloCameraRTCSignalingSession(self)
await plugin_session.initialize()
plugin_session = ArloCameraRTCSignalingSession(self)
await plugin_session.initialize()
scrypted_setup = {
"type": "offer",
"audio": {
"direction": "sendrecv" if self._can_push_to_talk() else "recvonly",
},
"video": {
"direction": "recvonly",
}
scrypted_setup = {
"type": "offer",
"audio": {
"direction": "sendrecv" if self._can_push_to_talk() else "recvonly",
},
"video": {
"direction": "recvonly",
}
plugin_setup = {}
}
plugin_setup = {}
scrypted_offer = await scrypted_session.createLocalDescription("offer", scrypted_setup, sendIceCandidate=plugin_session.addIceCandidate)
self.logger.info(f"Scrypted offer sdp:\n{scrypted_offer['sdp']}")
await plugin_session.setRemoteDescription(scrypted_offer, plugin_setup)
plugin_answer = await plugin_session.createLocalDescription("answer", plugin_setup, scrypted_session.sendIceCandidate)
self.logger.info(f"Scrypted answer sdp:\n{plugin_answer['sdp']}")
await scrypted_session.setRemoteDescription(plugin_answer, scrypted_setup)
scrypted_offer = await scrypted_session.createLocalDescription("offer", scrypted_setup, sendIceCandidate=plugin_session.addIceCandidate)
self.logger.info(f"Scrypted offer sdp:\n{scrypted_offer['sdp']}")
await plugin_session.setRemoteDescription(scrypted_offer, plugin_setup)
plugin_answer = await plugin_session.createLocalDescription("answer", plugin_setup, scrypted_session.sendIceCandidate)
self.logger.info(f"Scrypted answer sdp:\n{plugin_answer['sdp']}")
await scrypted_session.setRemoteDescription(plugin_answer, scrypted_setup)
return ArloCameraRTCSessionControl(plugin_session)
except Exception as e:
self.logger.error(e)
return ArloCameraRTCSessionControl(plugin_session)
async def startIntercom(self, media):
async def startIntercom(self, media) -> None:
self.logger.info("Starting intercom")
self.intercom_session = ArloCameraRTCSignalingSession(self)
await self.intercom_session.initialize_push_to_talk(media)
async def stopIntercom(self):
async def stopIntercom(self) -> None:
self.logger.info("Stopping intercom")
if self.intercom_session is not None:
await self.intercom_session.shutdown()
self.intercom_session = None
def _can_push_to_talk(self):
def _can_push_to_talk(self) -> bool:
# Right now, only implement push to talk for basestation cameras
return self.arlo_device["deviceId"] != self.arlo_device["parentId"]
async def getVideoClip(self, videoId: str) -> MediaObject:
self.logger.info(f"Getting video clip {videoId}")
id_as_time = int(videoId) / 1000.0
start = datetime.fromtimestamp(id_as_time) - timedelta(seconds=10)
end = datetime.fromtimestamp(id_as_time) + timedelta(seconds=10)
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
for recording in library:
if videoId == recording["name"]:
return await scrypted_sdk.mediaManager.createMediaObjectFromUrl(recording["presignedContentUrl"])
self.logger.warn(f"Clip {videoId} not found")
return None
async def getVideoClipThumbnail(self, thumbnailId: str) -> MediaObject:
self.logger.info(f"Getting video clip thumbnail {thumbnailId}")
id_as_time = int(thumbnailId) / 1000.0
start = datetime.fromtimestamp(id_as_time) - timedelta(seconds=10)
end = datetime.fromtimestamp(id_as_time) + timedelta(seconds=10)
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
for recording in library:
if thumbnailId == recording["name"]:
return await scrypted_sdk.mediaManager.createMediaObjectFromUrl(recording["presignedThumbnailUrl"])
self.logger.warn(f"Clip thumbnail {thumbnailId} not found")
return None
async def getVideoClips(self, options: VideoClipOptions = None) -> List[VideoClip]:
self.logger.info(f"Fetching remote video clips {options}")
start = datetime.fromtimestamp(options["startTime"] / 1000.0)
end = datetime.fromtimestamp(options["endTime"] / 1000.0)
library = self.provider.arlo.GetLibrary(self.arlo_device, start, end)
clips = []
for recording in library:
clip = {
"duration": recording["mediaDurationSecond"] * 1000.0,
"id": recording["name"],
"thumbnailId": recording["name"],
"videoId": recording["name"],
"startTime": recording["utcCreatedDate"],
"description": recording["reason"],
"resources": {
"thumbnail": {
"href": recording["presignedThumbnailUrl"],
},
"video": {
"href": recording["presignedContentUrl"],
},
},
}
clips.append(clip)
if options.get("reverseOrder"):
clips.reverse()
return clips
@async_print_exception_guard
async def removeVideoClips(self, videoClipIds: List[str]) -> None:
# Arlo does support deleting, but let's be safe and disable that
raise Exception("deleting Arlo video clips is not implemented by this plugin")
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
if (nativeId.endswith("spotlight") and self.has_spotlight) or (nativeId.endswith("floodlight") and self.has_floodlight):
return self.get_or_create_spotlight_or_floodlight()
if nativeId.endswith("vss") and self.has_siren:
return self.get_or_create_vss()
return None
def get_or_create_spotlight_or_floodlight(self) -> ArloSpotlight:
if self.has_spotlight:
light_id = f'{self.arlo_device["deviceId"]}.spotlight'
if not self.light:
self.light = ArloSpotlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
elif self.has_floodlight:
light_id = f'{self.arlo_device["deviceId"]}.floodlight'
if not self.light:
self.light = ArloFloodlight(light_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.light
def get_or_create_vss(self) -> ArloSirenVirtualSecuritySystem:
if self.has_siren:
vss_id = f'{self.arlo_device["deviceId"]}.vss'
if not self.vss:
self.vss = ArloSirenVirtualSecuritySystem(vss_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.vss
class ArloCameraRTCSignalingSession(BackgroundTaskMixin):
def __init__(self, camera):
@@ -330,7 +589,7 @@ class ArloCameraRTCSignalingSession(BackgroundTaskMixin):
self.logger.info("Initializing push to talk")
session_id, ice_servers = self.provider.arlo.StartPushToTalk(self.arlo_basestation, self.arlo_device)
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")
self.logger.debug(f"Received ice servers: {[ice['url'] for ice in ice_servers]}")
cfg = scrypted_arlo_go.WebRTCConfiguration(
ICEServers=scrypted_arlo_go.Slice_webrtc_ICEServer([
@@ -372,7 +631,7 @@ class ArloCameraRTCSignalingSession(BackgroundTaskMixin):
self.logger.debug("Starting audio track forwarder")
self.scrypted_pc.ForwardAudioTo(self.arlo_pc)
self.logger.debug("Started audio track forwarder")
self.sdp_answered = False
offer = self.arlo_pc.CreateOffer()

View File

@@ -1,29 +1,34 @@
from scrypted_sdk.types import BinarySensor, ScryptedInterface
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import BinarySensor, ScryptedInterface, ScryptedDeviceType
from .camera import ArloCamera
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
class ArloDoorbell(ArloCamera, BinarySensor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.start_doorbell_subscription()
def start_doorbell_subscription(self):
def start_doorbell_subscription(self) -> None:
def callback(doorbellPressed):
self.binaryState = doorbellPressed
return self.stop_subscriptions
self.register_task(
self.provider.arlo.SubscribeToDoorbellEvents(self.arlo_basestation, self.arlo_device, callback)
)
def get_applicable_interfaces(self):
def get_device_type(self) -> str:
return ScryptedDeviceType.Doorbell.value
def get_applicable_interfaces(self) -> List[str]:
camera_interfaces = super().get_applicable_interfaces()
camera_interfaces.append(ScryptedInterface.BinarySensor.value)
model_id = self.arlo_device['properties']['modelId'].lower()
if model_id.startswith("avd1001"):
camera_interfaces.remove(ScryptedInterface.Battery.value)
return camera_interfaces

View File

@@ -1,5 +1,4 @@
import logging
import sys
class ScryptedDeviceLoggingWrapper(logging.Handler):
@@ -20,7 +19,7 @@ def createScryptedLogger(scrypted_device, name):
logger.setLevel(logging.INFO)
# configure logger to output to scrypted's log stream
# configure logger to output to scrypted's log stream
sh = ScryptedDeviceLoggingWrapper(scrypted_device)
# log formatting

View File

@@ -6,27 +6,31 @@ import logging
import re
import requests
import traceback
from typing import List
import scrypted_sdk
from scrypted_sdk import ScryptedDeviceBase
from scrypted_sdk.types import Settings, DeviceProvider, DeviceDiscovery, ScryptedInterface, ScryptedDeviceType
from scrypted_sdk.types import Setting, SettingValue, Settings, DeviceProvider, ScryptedInterface
from .arlo import Arlo
from .arlo.arlo_async import change_stream_class
from .arlo.logging import logger as arlo_lib_logger
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin, async_print_exception_guard
from .camera import ArloCamera
from .doorbell import ArloDoorbell
from .logging import ScryptedDeviceLoggerMixin
from .util import BackgroundTaskMixin
from .basestation import ArloBasestation
from .base import ArloDeviceBase
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, ScryptedDeviceLoggerMixin, BackgroundTaskMixin):
arlo_cameras = None
arlo_basestations = None
_arlo_mfa_code = None
scrypted_devices = None
_arlo = None
_arlo_mfa_complete_auth = None
device_discovery_lock: asyncio.Lock = None
plugin_verbosity_choices = {
"Normal": logging.INFO,
@@ -37,7 +41,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
mfa_strategy_choices = ["Manual", "IMAP"]
def __init__(self, nativeId=None):
def __init__(self, nativeId: str = None) -> None:
super().__init__(nativeId=nativeId)
self.logger_name = "provider"
@@ -47,6 +51,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.imap = None
self.imap_signal = None
self.imap_skip_emails = None
self.device_discovery_lock = asyncio.Lock()
self.propagate_verbosity()
self.propagate_transport()
@@ -60,28 +65,28 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
asyncio.get_event_loop().call_soon(load, self)
self.create_task(self.onDeviceEvent(ScryptedInterface.Settings.value, None))
def print(self, *args, **kwargs):
def print(self, *args, **kwargs) -> None:
"""Overrides the print() from ScryptedDeviceBase to avoid double-printing in the main plugin console."""
print(*args, **kwargs)
@property
def arlo_username(self):
def arlo_username(self) -> str:
return self.storage.getItem("arlo_username")
@property
def arlo_password(self):
def arlo_password(self) -> str:
return self.storage.getItem("arlo_password")
@property
def arlo_auth_headers(self):
def arlo_auth_headers(self) -> str:
return self.storage.getItem("arlo_auth_headers")
@property
def arlo_user_id(self):
def arlo_user_id(self) -> str:
return self.storage.getItem("arlo_user_id")
@property
def arlo_transport(self):
def arlo_transport(self) -> str:
transport = self.storage.getItem("arlo_transport")
if transport is None or transport not in ArloProvider.arlo_transport_choices:
transport = "SSE"
@@ -89,7 +94,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return transport
@property
def plugin_verbosity(self):
def plugin_verbosity(self) -> str:
verbosity = self.storage.getItem("plugin_verbosity")
if verbosity is None or verbosity not in ArloProvider.plugin_verbosity_choices:
verbosity = "Normal"
@@ -97,7 +102,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return verbosity
@property
def mfa_strategy(self):
def mfa_strategy(self) -> str:
strategy = self.storage.getItem("mfa_strategy")
if strategy is None or strategy not in ArloProvider.mfa_strategy_choices:
strategy = "Manual"
@@ -105,7 +110,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return strategy
@property
def refresh_interval(self):
def refresh_interval(self) -> int:
interval = self.storage.getItem("refresh_interval")
if interval is None:
interval = 90
@@ -113,11 +118,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return int(interval)
@property
def imap_mfa_host(self):
def imap_mfa_host(self) -> str:
return self.storage.getItem("imap_mfa_host")
@property
def imap_mfa_port(self):
def imap_mfa_port(self) -> int:
port = self.storage.getItem("imap_mfa_port")
if port is None:
port = 993
@@ -125,23 +130,23 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return int(port)
@property
def imap_mfa_username(self):
def imap_mfa_username(self) -> str:
return self.storage.getItem("imap_mfa_username")
@property
def imap_mfa_password(self):
def imap_mfa_password(self) -> str:
return self.storage.getItem("imap_mfa_password")
@property
def imap_mfa_interval(self):
def imap_mfa_interval(self) -> int:
interval = self.storage.getItem("imap_mfa_interval")
if interval is None:
interval = 7
interval = 7
self.storage.setItem("imap_mfa_interval", interval)
return int(interval)
@property
def arlo(self):
def arlo(self) -> Arlo:
if self._arlo is not None:
if self._arlo_mfa_complete_auth is not None:
if self._arlo_mfa_code == "":
@@ -149,7 +154,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.logger.info("Completing Arlo MFA...")
self._arlo_mfa_complete_auth(self._arlo_mfa_code)
self._arlo_mfa_complete_auth = None
self._arlo_mfa_complete_auth = None
self._arlo_mfa_code = None
self.logger.info("Arlo MFA done")
@@ -162,7 +167,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
if not self.arlo_username or not self.arlo_password:
return None
self.logger.info("Trying to initialize Arlo client...")
try:
self._arlo = Arlo(self.arlo_username, self.arlo_password)
@@ -183,16 +188,13 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self._arlo_mfa_code = None
return None
async def do_arlo_setup(self):
async def do_arlo_setup(self) -> None:
try:
await self.discoverDevices()
await self.discover_devices()
await self.arlo.Subscribe([
(self.arlo_basestations[camera["parentId"]], camera) for camera in self.arlo_cameras.values()
])
for nativeId in self.arlo_cameras.keys():
await self.getDevice(nativeId)
self.arlo.event_stream.set_refresh_interval(self.refresh_interval)
except requests.exceptions.HTTPError as e:
traceback.print_exc()
@@ -204,7 +206,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
except Exception as e:
traceback.print_exc()
def invalidate_arlo_client(self):
def invalidate_arlo_client(self) -> None:
if self._arlo is not None:
self._arlo.Unsubscribe()
self._arlo = None
@@ -213,10 +215,10 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.storage.setItem("arlo_auth_headers", "")
self.storage.setItem("arlo_user_id", "")
def get_current_log_level(self):
def get_current_log_level(self) -> int:
return ArloProvider.plugin_verbosity_choices[self.plugin_verbosity]
def propagate_verbosity(self):
def propagate_verbosity(self) -> None:
self.print(f"Setting plugin verbosity to {self.plugin_verbosity}")
log_level = self.get_current_log_level()
self.logger.setLevel(log_level)
@@ -224,11 +226,11 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
device.logger.setLevel(log_level)
arlo_lib_logger.setLevel(log_level)
def propagate_transport(self):
def propagate_transport(self) -> None:
self.print(f"Setting plugin transport to {self.arlo_transport}")
change_stream_class(self.arlo_transport)
def initialize_imap(self):
def initialize_imap(self) -> None:
if not self.imap_mfa_host or not self.imap_mfa_port or \
not self.imap_mfa_username or not self.imap_mfa_password or \
not self.imap_mfa_interval:
@@ -245,7 +247,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
res, _ = self.imap.select(mailbox="INBOX", readonly=True)
if res.lower() != "ok":
raise Exception(f"IMAP failed to fetch INBOX: {res}")
# fetch existing arlo emails so we skip them going forward
res, self.imap_skip_emails = self.imap.search(None, "FROM", "do_not_reply@arlo.com")
if res.lower() != "ok":
@@ -258,14 +260,14 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.imap_signal = asyncio.Queue()
self.create_task(self.imap_relogin_loop())
def exit_imap(self):
def exit_imap(self) -> None:
if self.imap_signal:
self.imap_signal.put_nowait(None)
self.imap_signal = None
self.imap_skip_emails = None
self.imap = None
async def imap_relogin_loop(self):
async def imap_relogin_loop(self) -> None:
imap_signal = self.imap_signal
self.logger.info(f"Starting IMAP refresh loop {id(imap_signal)}")
while True:
@@ -368,7 +370,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.logger.info(f"Exiting IMAP refresh loop {id(imap_signal)}")
return
async def getSettings(self):
async def getSettings(self) -> List[Setting]:
results = [
{
"group": "General",
@@ -447,7 +449,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
"value": self.imap_mfa_interval,
}
])
results.extend([
{
"group": "General",
@@ -469,17 +471,17 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
{
"group": "General",
"key": "plugin_verbosity",
"title": "Plugin Verbosity",
"description": "Select the verbosity of this plugin. 'Verbose' will show debugging messages, "
"including events received from connected Arlo cameras.",
"value": self.plugin_verbosity,
"choices": sorted(self.plugin_verbosity_choices.keys()),
"title": "Verbose Logging",
"description": "Enable this option to show debug messages, including events received from connected Arlo cameras.",
"value": self.plugin_verbosity == "Verbose",
"type": "boolean",
},
])
return results
async def putSetting(self, key, value):
@async_print_exception_guard
async def putSetting(self, key: str, value: SettingValue) -> None:
if not self.validate_setting(key, value):
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
return
@@ -490,13 +492,14 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
elif key == "force_reauth":
# force arlo client to be invalidated and reloaded
self.invalidate_arlo_client()
elif key == "plugin_verbosity":
self.storage.setItem(key, "Verbose" if value == "true" or value == True else "Normal")
self.propagate_verbosity()
skip_arlo_client = True
else:
self.storage.setItem(key, value)
if key == "plugin_verbosity":
self.propagate_verbosity()
skip_arlo_client = True
elif key == "arlo_transport":
if key == "arlo_transport":
self.propagate_transport()
# force arlo client to be invalidated and reloaded, but
# keep any mfa codes
@@ -525,7 +528,7 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
_ = self.arlo
await self.onDeviceEvent(ScryptedInterface.Settings.value, None)
def validate_setting(self, key, val):
def validate_setting(self, key: str, val: SettingValue) -> bool:
if key == "refresh_interval":
try:
val = int(val)
@@ -555,7 +558,12 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
return False
return True
async def discoverDevices(self, duration=0):
@async_print_exception_guard
async def discover_devices(self) -> None:
async with self.device_discovery_lock:
return await self.discover_devices_impl()
async def discover_devices_impl(self) -> None:
if not self.arlo:
raise Exception("Arlo client not connected, cannot discover devices")
@@ -564,70 +572,126 @@ class ArloProvider(ScryptedDeviceBase, Settings, DeviceProvider, DeviceDiscovery
self.arlo_basestations = {}
self.scrypted_devices = {}
camera_devices = []
provider_to_device_map = {}
basestations = self.arlo.GetDevices(['basestation', 'siren'])
for basestation in basestations:
self.arlo_basestations[basestation["deviceId"]] = basestation
nativeId = basestation["deviceId"]
self.logger.debug(f"Adding {nativeId}")
if nativeId in self.arlo_basestations:
self.logger.info(f"Skipping basestation {nativeId} ({basestation['modelId']}) as it has already been added")
continue
self.arlo_basestations[nativeId] = basestation
device = await self.getDevice_impl(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.debug(f"Interfaces for {nativeId} ({basestation['modelId']}): {scrypted_interfaces}")
# for basestations, we want to add them to the top level DeviceProvider
provider_to_device_map.setdefault(None, []).append(manifest)
# we want to trickle discover them so they are added without deleting all existing
# root level devices - this is for backward compatibility
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
# add any builtin child devices and trickle discover them
child_manifests = device.get_builtin_child_device_manifests()
for child_manifest in child_manifests:
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
self.logger.info(f"Discovered {len(basestations)} basestations")
devices = []
cameras = self.arlo.GetDevices(['camera', "arloq", "arloqs", "doorbell"])
for camera in cameras:
nativeId = camera["deviceId"]
self.logger.debug(f"Adding {nativeId}")
if camera["deviceId"] != camera["parentId"] and camera["parentId"] not in self.arlo_basestations:
self.logger.info(f"Skipping camera {camera['deviceId']} because its basestation was not found")
self.logger.info(f"Skipping camera {camera['deviceId']} ({camera['modelId']}) because its basestation was not found")
continue
if camera["deviceId"] == camera["parentId"]:
self.arlo_basestations[camera["deviceId"]] = camera
nativeId = camera["deviceId"]
if nativeId in self.arlo_cameras:
self.logger.info(f"Skipping camera {nativeId} ({camera['modelId']}) as it has already been added")
continue
self.arlo_cameras[nativeId] = camera
scrypted_interfaces = (await self.getDevice(nativeId)).get_applicable_interfaces()
if camera["deviceId"] == camera["parentId"]:
# these are standalone cameras with no basestation, so they act as their
# own basestation
self.arlo_basestations[camera["deviceId"]] = camera
device = await self.getDevice_impl(nativeId)
scrypted_interfaces = device.get_applicable_interfaces()
manifest = device.get_device_manifest()
self.logger.debug(f"Interfaces for {nativeId} ({camera['modelId']}): {scrypted_interfaces}")
device = {
"info": {
"model": f"{camera['properties']['modelId']} ({camera['properties'].get('hwVersion', '')})".strip(),
"manufacturer": "Arlo",
"firmware": camera.get("firmwareVersion"),
"serialNumber": camera["deviceId"],
},
"nativeId": camera["deviceId"],
"name": camera["deviceName"],
"interfaces": scrypted_interfaces,
"type": ScryptedDeviceType.Camera.value,
"providerNativeId": self.nativeId,
}
if camera["deviceId"] == camera["parentId"]:
provider_to_device_map.setdefault(None, []).append(manifest)
else:
provider_to_device_map.setdefault(camera["parentId"], []).append(manifest)
devices.append(device)
# trickle discover this camera so it exists for later steps
await scrypted_sdk.deviceManager.onDeviceDiscovered(manifest)
await scrypted_sdk.deviceManager.onDevicesChanged({
"devices": devices,
})
# add any builtin child devices and trickle discover them
child_manifests = device.get_builtin_child_device_manifests()
for child_manifest in child_manifests:
await scrypted_sdk.deviceManager.onDeviceDiscovered(child_manifest)
provider_to_device_map.setdefault(child_manifest["providerNativeId"], []).append(child_manifest)
if len(cameras) != len(devices):
self.logger.info(f"Discovered {len(cameras)} cameras, but only {len(devices)} are usable")
camera_devices.append(manifest)
if len(cameras) != len(camera_devices):
self.logger.info(f"Discovered {len(cameras)} cameras, but only {len(camera_devices)} are usable")
else:
self.logger.info(f"Discovered {len(cameras)} cameras")
async def getDevice(self, nativeId):
for provider_id in provider_to_device_map.keys():
if provider_id is None:
continue
await scrypted_sdk.deviceManager.onDevicesChanged({
"devices": provider_to_device_map[provider_id],
"providerNativeId": provider_id,
})
# ensure devices at the root match all that was discovered
await scrypted_sdk.deviceManager.onDevicesChanged({
"devices": provider_to_device_map[None]
})
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
async with self.device_discovery_lock:
return await self.getDevice_impl(nativeId)
async def getDevice_impl(self, nativeId: str) -> ArloDeviceBase:
ret = self.scrypted_devices.get(nativeId, None)
if ret is None:
ret = self.create_camera(nativeId)
ret = self.create_device(nativeId)
if ret is not None:
self.scrypted_devices[nativeId] = ret
return ret
def create_camera(self, nativeId):
if nativeId not in self.arlo_cameras:
def create_device(self, nativeId: str) -> ArloDeviceBase:
if nativeId not in self.arlo_cameras and nativeId not in self.arlo_basestations:
self.logger.warning(f"Cannot create device for nativeId {nativeId}, maybe it hasn't been loaded yet?")
return None
arlo_camera = self.arlo_cameras[nativeId]
if arlo_camera["parentId"] not in self.arlo_basestations:
arlo_device = self.arlo_cameras.get(nativeId)
if not arlo_device:
# this is a basestation, so build the basestation object
arlo_device = self.arlo_basestations[nativeId]
return ArloBasestation(nativeId, arlo_device, self)
if arlo_device["parentId"] not in self.arlo_basestations:
self.logger.warning(f"Cannot create camera with nativeId {nativeId} when {arlo_device['parentId']} is not a valid basestation")
return None
arlo_basestation = self.arlo_basestations[arlo_camera["parentId"]]
arlo_basestation = self.arlo_basestations[arlo_device["parentId"]]
if arlo_camera["deviceType"] == "doorbell":
return ArloDoorbell(nativeId, arlo_camera, arlo_basestation, self)
if arlo_device["deviceType"] == "doorbell":
return ArloDoorbell(nativeId, arlo_device, arlo_basestation, self)
else:
return ArloCamera(nativeId, arlo_camera, arlo_basestation, self)
return ArloCamera(nativeId, arlo_device, arlo_basestation, self)

View File

@@ -0,0 +1,72 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import OnOff, SecuritySystemMode, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .util import async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .vss import ArloSirenVirtualSecuritySystem
class ArloSiren(ArloDeviceBase, OnOff):
vss: ArloSirenVirtualSecuritySystem = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, vss: ArloSirenVirtualSecuritySystem) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.vss = vss
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.OnOff.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.Siren.value
@async_print_exception_guard
async def turnOn(self) -> None:
from .basestation import ArloBasestation
self.logger.info("Turning on")
if self.vss.securitySystemState["mode"] == SecuritySystemMode.Disarmed.value:
self.logger.info("Virtual security system is disarmed, ignoring trigger")
# set and unset this property to force homekit to display the
# switch as off
self.on = True
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": False,
}
return
if isinstance(self.vss.parent, ArloBasestation):
self.logger.debug("Parent device is a basestation")
self.provider.arlo.SirenOn(self.arlo_basestation)
else:
self.logger.debug("Parent device is a camera")
self.provider.arlo.SirenOn(self.arlo_basestation, self.arlo_device)
self.on = True
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": True,
}
@async_print_exception_guard
async def turnOff(self) -> None:
from .basestation import ArloBasestation
self.logger.info("Turning off")
if isinstance(self.vss.parent, ArloBasestation):
self.provider.arlo.SirenOff(self.arlo_basestation)
else:
self.provider.arlo.SirenOff(self.arlo_basestation, self.arlo_device)
self.on = False
self.vss.securitySystemState = {
**self.vss.securitySystemState,
"triggered": False,
}

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import OnOff, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .util import async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .camera import ArloCamera
class ArloSpotlight(ArloDeviceBase, OnOff):
camera: ArloCamera = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, camera: ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.camera = camera
def get_applicable_interfaces(self) -> List[str]:
return [ScryptedInterface.OnOff.value]
def get_device_type(self) -> str:
return ScryptedDeviceType.Light.value
@async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.SpotlightOn(self.arlo_basestation, self.arlo_device)
self.on = True
@async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.SpotlightOff(self.arlo_basestation, self.arlo_device)
self.on = False
class ArloFloodlight(ArloSpotlight):
@async_print_exception_guard
async def turnOn(self) -> None:
self.logger.info("Turning on")
self.provider.arlo.FloodlightOn(self.arlo_basestation, self.arlo_device)
self.on = True
@async_print_exception_guard
async def turnOff(self) -> None:
self.logger.info("Turning off")
self.provider.arlo.FloodlightOff(self.arlo_basestation, self.arlo_device)
self.on = False

View File

@@ -1,13 +1,14 @@
import asyncio
import traceback
class BackgroundTaskMixin:
def create_task(self, coroutine):
def create_task(self, coroutine) -> asyncio.Task:
task = asyncio.get_event_loop().create_task(coroutine)
self.register_task(task)
return task
def register_task(self, task):
def register_task(self, task) -> None:
if not hasattr(self, "background_tasks"):
self.background_tasks = set()
@@ -21,6 +22,18 @@ class BackgroundTaskMixin:
task.add_done_callback(print_exception)
task.add_done_callback(self.background_tasks.discard)
def cancel_pending_tasks(self):
def cancel_pending_tasks(self) -> None:
if not hasattr(self, "background_tasks"):
return
for task in self.background_tasks:
task.cancel()
task.cancel()
def async_print_exception_guard(fn):
"""Decorator to print an exception's stack trace before re-raising the exception."""
async def wrapped(*args, **kwargs):
try:
return await fn(*args, **kwargs)
except Exception:
traceback.print_exc()
raise
return wrapped

View File

@@ -0,0 +1,156 @@
from __future__ import annotations
import asyncio
from typing import List, TYPE_CHECKING
from scrypted_sdk.types import Device, DeviceProvider, Setting, Settings, SettingValue, SecuritySystem, SecuritySystemMode, Readme, ScryptedInterface, ScryptedDeviceType
from .base import ArloDeviceBase
from .siren import ArloSiren
from .util import async_print_exception_guard
if TYPE_CHECKING:
# https://adamj.eu/tech/2021/05/13/python-type-hints-how-to-fix-circular-imports/
from .provider import ArloProvider
from .basestation import ArloBasestation
from .camera import ArloCamera
class ArloSirenVirtualSecuritySystem(ArloDeviceBase, SecuritySystem, Settings, Readme, DeviceProvider):
"""A virtual, emulated security system that controls when scrypted events can trip the real physical siren."""
SUPPORTED_MODES = [SecuritySystemMode.AwayArmed.value, SecuritySystemMode.HomeArmed.value, SecuritySystemMode.Disarmed.value]
siren: ArloSiren = None
parent: ArloBasestation | ArloCamera = None
def __init__(self, nativeId: str, arlo_device: dict, arlo_basestation: dict, provider: ArloProvider, parent: ArloBasestation | ArloCamera) -> None:
super().__init__(nativeId=nativeId, arlo_device=arlo_device, arlo_basestation=arlo_basestation, provider=provider)
self.parent = parent
self.create_task(self.delayed_init())
@property
def mode(self) -> str:
mode = self.storage.getItem("mode")
if mode is None or mode not in ArloSirenVirtualSecuritySystem.SUPPORTED_MODES:
mode = SecuritySystemMode.Disarmed.value
return mode
@mode.setter
def mode(self, mode: str) -> None:
if mode not in ArloSirenVirtualSecuritySystem.SUPPORTED_MODES:
raise ValueError(f"invalid mode {mode}")
self.storage.setItem("mode", mode)
self.securitySystemState = {
**self.securitySystemState,
"mode": mode,
}
self.create_task(self.onDeviceEvent(ScryptedInterface.Settings.value, None))
async def delayed_init(self) -> None:
iterations = 1
while not self.stop_subscriptions:
if iterations > 100:
self.logger.error("Delayed init exceeded iteration limit, giving up")
return
try:
self.securitySystemState = {
"supportedModes": ArloSirenVirtualSecuritySystem.SUPPORTED_MODES,
"mode": self.mode,
}
return
except Exception as e:
self.logger.debug(f"Delayed init failed, will try again: {e}")
await asyncio.sleep(0.1)
iterations += 1
def get_applicable_interfaces(self) -> List[str]:
return [
ScryptedInterface.SecuritySystem.value,
ScryptedInterface.DeviceProvider.value,
ScryptedInterface.Settings.value,
ScryptedInterface.Readme.value,
]
def get_device_type(self) -> str:
return ScryptedDeviceType.SecuritySystem.value
def get_builtin_child_device_manifests(self) -> List[Device]:
siren = self.get_or_create_siren()
return [
{
"info": {
"model": f"{self.arlo_device['modelId']} {self.arlo_device['properties'].get('hwVersion', '')}".strip(),
"manufacturer": "Arlo",
"firmware": self.arlo_device.get("firmwareVersion"),
"serialNumber": self.arlo_device["deviceId"],
},
"nativeId": siren.nativeId,
"name": f'{self.arlo_device["deviceName"]} Siren',
"interfaces": siren.get_applicable_interfaces(),
"type": siren.get_device_type(),
"providerNativeId": self.nativeId,
}
]
async def getSettings(self) -> List[Setting]:
return [
{
"key": "mode",
"title": "Arm Mode",
"description": "If disarmed, the associated siren will not be physically triggered even if toggled.",
"value": self.mode,
"choices": ArloSirenVirtualSecuritySystem.SUPPORTED_MODES,
},
]
async def putSetting(self, key: str, value: SettingValue) -> None:
if key != "mode":
raise ValueError(f"invalid setting {key}")
self.mode = value
if self.mode == SecuritySystemMode.Disarmed.value:
await self.get_or_create_siren().turnOff()
async def getReadmeMarkdown(self) -> str:
return """
# Virtual Security System for Arlo Sirens
This security system device is not a real physical device, but a virtual, emulated device provided by the Arlo Scrypted plugin. Its purpose is to grant security system semantics of Arm/Disarm to avoid the accidental, unwanted triggering of the real physical siren through integrations such as Homekit.
To allow the siren to trigger, set the Arm Mode to any of the Armed options. When Disarmed, any triggers of the siren will be ignored. Switching modes will not perform any changes to Arlo cloud or your Arlo account, but rather only to this Scrypted device.
If this virtual security system is synced to Homekit, the siren device will be merged into the same security system accessory as a switch. The siren device will not be added as a separate accessory. To access the siren as a switch without the security system, disable syncing of the virtual security system and enable syncing of the siren, then ensure that the virtual security system is armed manually in its settings in Scrypted.
""".strip()
async def getDevice(self, nativeId: str) -> ArloDeviceBase:
if not nativeId.endswith("siren"):
return None
return self.get_or_create_siren()
def get_or_create_siren(self) -> ArloSiren:
siren_id = f'{self.arlo_device["deviceId"]}.siren'
if not self.siren:
self.siren = ArloSiren(siren_id, self.arlo_device, self.arlo_basestation, self.provider, self)
return self.siren
@async_print_exception_guard
async def armSecuritySystem(self, mode: SecuritySystemMode) -> None:
self.logger.info(f"Arming {mode}")
self.mode = mode
self.securitySystemState = {
**self.securitySystemState,
"mode": mode,
}
if mode == SecuritySystemMode.Disarmed.value:
await self.get_or_create_siren().turnOff()
@async_print_exception_guard
async def disarmSecuritySystem(self) -> None:
self.logger.info(f"Disarming")
self.mode = SecuritySystemMode.Disarmed.value
self.securitySystemState = {
**self.securitySystemState,
"mode": SecuritySystemMode.Disarmed.value,
}
await self.get_or_create_siren().turnOff()

View File

@@ -1,6 +1,7 @@
paho-mqtt==1.6.1
sseclient==0.0.22
requests
requests==2.28.2
cachetools==5.3.0
scrypted-arlo-go==0.0.1
--extra-index-url=https://www.piwheels.org/simple/
--extra-index-url=https://bjia56.github.io/scrypted-arlo-go/

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +1,49 @@
{
"name": "@scrypted/bticino",
"version": "0.0.5",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
"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"
},
"keywords": [
"scrypted",
"plugin",
"sip"
],
"scrypted": {
"name": "BTicino SIP Plugin",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"DeviceCreator"
],
"pluginDependencies": [
"@scrypted/prebuffer-mixin",
"@scrypted/pam-diff",
"@scrypted/snapshot"
]
},
"dependencies": {
"@homebridge/camera-utils": "^2.0.4",
"rxjs": "^7.5.5",
"sdp": "^3.0.3",
"sip": "0.0.6",
"stun": "^2.1.0",
"ts-node": "^10.9.1",
"uuid": "^8.3.2"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.9.6",
"@types/uuid": "^8.3.4"
}
}
{
"name": "@scrypted/bticino",
"version": "0.0.7",
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
"build": "scrypted-webpack",
"prepublishOnly": "cross-env 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"
},
"keywords": [
"scrypted",
"plugin",
"sip"
],
"scrypted": {
"name": "BTicino SIP Plugin",
"type": "DeviceProvider",
"interfaces": [
"DeviceProvider",
"DeviceCreator"
],
"pluginDependencies": [
"@scrypted/prebuffer-mixin",
"@scrypted/pam-diff",
"@scrypted/snapshot"
]
},
"dependencies": {
"@slyoldfox/sip": "^0.0.6-1",
"sdp": "^3.0.3",
"stun": "^2.1.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@scrypted/common": "file:../../common",
"@scrypted/sdk": "file:../../sdk",
"@types/node": "^16.9.6",
"@types/uuid": "^8.3.4",
"cross-env": "^7.0.3",
"ts-node": "^10.9.1"
}
}

View File

@@ -0,0 +1,407 @@
import { closeQuiet, createBindZero, listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { sleep } from '@scrypted/common/src/sleep';
import { RtspServer } from '@scrypted/common/src/rtsp-server';
import { addTrackControls } from '@scrypted/common/src/sdp-utils';
import sdk, { BinarySensor, Camera, DeviceProvider, FFmpegInput, HttpRequest, HttpRequestHandler, HttpResponse, Intercom, MediaObject, MediaStreamUrl, PictureOptions, ResponseMediaStreamOptions, ScryptedDevice, ScryptedDeviceBase, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips } from '@scrypted/sdk';
import { SipCallSession } from '../../sip/src/sip-call-session';
import { RtpDescription } from '../../sip/src/rtp-utils';
import { VoicemailHandler } from './bticino-voicemailHandler';
import { CompositeSipMessageHandler } from '../../sip/src/compositeSipMessageHandler';
import { SipHelper } from './sip-helper';
import child_process, { ChildProcess } from 'child_process';
import dgram from 'dgram';
import { BticinoStorageSettings } from './storage-settings';
import { BticinoSipPlugin } from './main';
import { BticinoSipLock } from './bticino-lock';
import { ffmpegLogInitialOutput, safeKillFFmpeg, safePrintFFmpegArguments } from '@scrypted/common/src/media-helpers';
import { PersistentSipManager } from './persistent-sip-manager';
import { InviteHandler } from './bticino-inviteHandler';
import { SipRequest } from '../../sip/src/sip-manager';
import { get } from 'http'
const STREAM_TIMEOUT = 65000;
const { mediaManager } = sdk;
export class BticinoSipCamera extends ScryptedDeviceBase implements DeviceProvider, Intercom, Camera, VideoCamera, Settings, BinarySensor, HttpRequestHandler, VideoClips {
private session: SipCallSession
private remoteRtpDescription: RtpDescription
private audioOutForwarder: dgram.Socket
private audioOutProcess: ChildProcess
private currentMedia: FFmpegInput | MediaStreamUrl
private currentMediaMimeType: string
private refreshTimeout: NodeJS.Timeout
public requestHandlers: CompositeSipMessageHandler = new CompositeSipMessageHandler()
public incomingCallRequest : SipRequest
private settingsStorage: BticinoStorageSettings = new BticinoStorageSettings( this )
public voicemailHandler : VoicemailHandler = new VoicemailHandler(this)
private inviteHandler : InviteHandler = new InviteHandler(this)
//TODO: randomize this
private keyAndSalt : string = "/qE7OPGKp9hVGALG2KcvKWyFEZfSSvm7bYVDjT8X"
//private decodedSrtpOptions : SrtpOptions = decodeSrtpOptions( this.keyAndSalt )
private persistentSipManager : PersistentSipManager
public doorbellWebhookUrl : string
public doorbellLockWebhookUrl : string
constructor(nativeId: string, public provider: BticinoSipPlugin) {
super(nativeId)
this.requestHandlers.add( this.voicemailHandler ).add( this.inviteHandler )
this.persistentSipManager = new PersistentSipManager( this );
(async() => {
this.doorbellWebhookUrl = await this.doorbellWebhookEndpoint()
this.doorbellLockWebhookUrl = await this.doorbellLockWebhookEndpoint()
})();
}
getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
return new Promise<VideoClip[]>( (resolve,reject ) => {
let c300x = SipHelper.getIntercomIp(this)
if( !c300x ) return []
get(`http://${c300x}:8080/videoclips?raw=true&startTime=${options.startTime/1000}&endTime=${options.endTime/1000}`, (res) => {
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
try {
const parsedData : [] = JSON.parse(rawData);
let videoClips : VideoClip[] = []
parsedData.forEach( (item) => {
let videoClip : VideoClip = {
id: item['file'],
startTime: parseInt(item['info']['UnixTime']) * 1000,
duration: item['info']['Duration'] * 1000,
//description: item['info']['Date'],
thumbnailId: item['file']
}
videoClips.push( videoClip )
} )
return resolve(videoClips)
} catch (e) {
reject(e.message)
console.error(e.message);
}
})
});
});
}
getVideoClip(videoId: string): Promise<MediaObject> {
let c300x = SipHelper.getIntercomIp(this)
const url = `http://${c300x}:8080/voicemail?msg=${videoId}/aswm.avi&raw=true`;
return mediaManager.createMediaObjectFromUrl(url);
}
getVideoClipThumbnail(thumbnailId: string): Promise<MediaObject> {
let c300x = SipHelper.sipOptions(this)
const url = `http://${c300x}:8080/voicemail?msg=${thumbnailId}/aswm.jpg&raw=true`;
return mediaManager.createMediaObjectFromUrl(url);
}
removeVideoClips(...videoClipIds: string[]): Promise<void> {
//TODO
throw new Error('Method not implemented.')
}
sipUnlock(): Promise<void> {
this.log.i("unlocking C300X door ")
return this.persistentSipManager.enable().then( (sipCall) => {
sipCall.message( '*8*19*20##' )
.then( () =>
sleep(1000)
.then( () => sipCall.message( '*8*20*20##' ) )
)
} )
}
getAswmStatus() : Promise<void> {
return this.persistentSipManager.enable().then( (sipCall) => {
sipCall.message( "GetAswmStatus!" )
} )
}
async takePicture(option?: PictureOptions): Promise<MediaObject> {
throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
}
async getPictureOptions(): Promise<PictureOptions[]> {
return
}
getSettings(): Promise<Setting[]> {
return this.settingsStorage.getSettings()
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.settingsStorage.putSetting(key, value)
}
async startIntercom(media: MediaObject): Promise<void> {
if (!this.session)
throw new Error("not in call");
this.stopIntercom();
const ffmpegInput: FFmpegInput = JSON.parse((await mediaManager.convertMediaObjectToBuffer(media, ScryptedMimeTypes.FFmpegInput)).toString());
const audioOutForwarder = await createBindZero()
this.audioOutForwarder = audioOutForwarder.server
audioOutForwarder.server.on('message', message => {
if( this.session )
this.session.audioSplitter.send(message, 40004, this.remoteRtpDescription.address)
return null
});
const args = ffmpegInput.inputArguments.slice();
args.push(
'-vn', '-dn', '-sn',
'-acodec', 'speex',
'-flags', '+global_header',
'-ac', '1',
'-ar', '8k',
'-f', 'rtp',
//'-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80',
//'-srtp_out_params', encodeSrtpOptions(this.decodedSrtpOptions),
`rtp://127.0.0.1:${audioOutForwarder.port}?pkt_size=188`,
);
this.console.log("===========================================")
safePrintFFmpegArguments( this.console, args )
this.console.log("===========================================")
const cp = child_process.spawn(await mediaManager.getFFmpegPath(), args);
ffmpegLogInitialOutput(this.console, cp)
this.audioOutProcess = cp;
cp.on('exit', () => this.console.log('two way audio ended'));
this.session.onCallEnded.subscribe(() => {
closeQuiet(audioOutForwarder.server);
safeKillFFmpeg(cp)
});
}
async stopIntercom(): Promise<void> {
closeQuiet(this.audioOutForwarder)
this.audioOutProcess?.kill('SIGKILL')
this.audioOutProcess = undefined
this.audioOutForwarder = undefined
}
resetStreamTimeout() {
this.log.d('starting/refreshing stream')
clearTimeout(this.refreshTimeout)
this.refreshTimeout = setTimeout(() => this.stopSession(), STREAM_TIMEOUT)
}
hasActiveCall() {
return this.session;
}
stopSession() {
if (this.session) {
this.log.d('ending sip session')
this.session.stop()
this.session = undefined
}
}
async getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
if( !SipHelper.sipOptions( this ) ) {
// Bail out fast when no options are set and someone enables prebuffering
throw new Error('Please configure from/to/domain settings')
}
if (options?.metadata?.refreshAt) {
if (!this.currentMedia?.mediaStreamOptions)
throw new Error("no stream to refresh");
const currentMedia = this.currentMedia
currentMedia.mediaStreamOptions.refreshAt = Date.now() + STREAM_TIMEOUT;
currentMedia.mediaStreamOptions.metadata = {
refreshAt: currentMedia.mediaStreamOptions.refreshAt
};
this.resetStreamTimeout()
return mediaManager.createMediaObject(currentMedia, this.currentMediaMimeType)
}
this.stopSession();
const { clientPromise: playbackPromise, port: playbackPort, url: clientUrl } = await listenZeroSingleClient()
const playbackUrl = clientUrl
playbackPromise.then(async (client) => {
client.setKeepAlive(true, 10000)
let sip: SipCallSession
try {
let rtsp: RtspServer;
const cleanup = () => {
client.destroy();
if (this.session === sip)
this.session = undefined
try {
this.log.d('cleanup(): stopping sip session.')
sip.stop()
}
catch (e) {
}
rtsp?.destroy()
}
client.on('close', cleanup)
client.on('error', cleanup)
let sipOptions = SipHelper.sipOptions( this )
sip = await this.persistentSipManager.session( sipOptions );
// Validate this sooner
if( !sip ) return Promise.reject("Cannot create session")
sip.onCallEnded.subscribe(cleanup)
// Call the C300X
this.remoteRtpDescription = await sip.callOrAcceptInvite(
( audio ) => {
return [
//TODO: Payload types are hardcoded
`m=audio 65000 RTP/SAVP 110`,
`a=rtpmap:110 speex/8000`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
]
}, ( video ) => {
if( false ) {
//TODO: implement later
return [
`m=video 0 RTP/SAVP 0`
]
} else {
return [
//TODO: Payload types are hardcoded
`m=video 65002 RTP/SAVP 96`,
`a=rtpmap:96 H264/90000`,
`a=fmtp:96 profile-level-id=42801F`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${this.keyAndSalt}`,
'a=recvonly'
]
}
}, this.incomingCallRequest );
this.incomingCallRequest = undefined
//let sdp: string = replacePorts(this.remoteRtpDescription.sdp, 0, 0 )
let sdp : string = [
"v=0",
"m=audio 5000 RTP/AVP 110",
"c=IN IP4 127.0.0.1",
"a=rtpmap:110 speex/8000/1",
"m=video 5002 RTP/AVP 96",
"c=IN IP4 127.0.0.1",
"a=rtpmap:96 H264/90000",
].join('\r\n')
//sdp = sdp.replaceAll(/a=crypto\:1.*/g, '')
//sdp = sdp.replaceAll(/RTP\/SAVP/g, 'RTP\/AVP')
//sdp = sdp.replaceAll('\r\n\r\n', '\r\n')
sdp = addTrackControls(sdp)
sdp = sdp.split('\n').filter(line => !line.includes('a=rtcp-mux')).join('\n')
if( sipOptions.debugSip )
this.log.d('SIP: Updated SDP:\n' + sdp);
client.write(sdp)
client.end()
this.session = sip
}
catch (e) {
this.console.error(e)
sip?.stop()
throw e;
}
});
this.resetStreamTimeout();
const mediaStreamOptions = Object.assign(this.getSipMediaStreamOptions(), {
refreshAt: Date.now() + STREAM_TIMEOUT,
});
const ffmpegInput: FFmpegInput = {
url: undefined,
container: 'sdp',
mediaStreamOptions,
inputArguments: [
'-f', 'sdp',
'-i', playbackUrl,
],
};
this.currentMedia = ffmpegInput;
this.currentMediaMimeType = ScryptedMimeTypes.FFmpegInput;
return mediaManager.createFFmpegMediaObject(ffmpegInput);
}
getSipMediaStreamOptions(): ResponseMediaStreamOptions {
return {
id: 'sip',
name: 'SIP',
// this stream is NOT scrypted blessed due to wackiness in the h264 stream.
// tool: "scrypted",
container: 'sdp',
audio: {
// this is a hint to let homekit, et al, know that it's speex audio and needs transcoding.
codec: 'speex',
},
source: 'cloud', // to disable prebuffering
userConfigurable: false,
};
}
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
return [
this.getSipMediaStreamOptions(),
]
}
async getDevice(nativeId: string) : Promise<BticinoSipLock> {
return new BticinoSipLock(this)
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
reset() {
this.console.log("Reset the incoming call request")
this.incomingCallRequest = undefined
this.binaryState = false
}
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
if (request.url.endsWith('/pressed')) {
this.binaryState = true
setTimeout( () => {
// Assumption that flexisip only holds this call active for 20 seconds ... might be revised
this.reset()
}, 20 * 1000 )
response.send('Success', {
code: 200,
});
} else {
response.send('Unsupported operation', {
code: 400,
});
}
}
private async doorbellWebhookEndpoint(): Promise<string> {
let webhookUrl = await sdk.endpointManager.getLocalEndpoint( this.nativeId, { insecure: false, public: true });
let endpoints = ["/pressed"]
this.console.log( webhookUrl + " , endpoints: " + endpoints.join(' - ') )
return `${webhookUrl}`;
}
private async doorbellLockWebhookEndpoint(): Promise<string> {
let webhookUrl = await sdk.endpointManager.getLocalEndpoint(this.nativeId + '-lock', { insecure: false, public: true });
let endpoints = ["/lock", "/unlock", "/unlocked", "/locked"]
this.console.log( webhookUrl + " -> endpoints: " + endpoints.join(' - ') )
return `${webhookUrl}`;
}
}

View File

@@ -0,0 +1,32 @@
import { SipRequestHandler, SipRequest } from "../../sip/src/sip-manager"
import { BticinoSipCamera } from "./bticino-camera"
import { stringifyUri } from '@slyoldfox/sip'
export class InviteHandler extends SipRequestHandler {
constructor( private sipCamera : BticinoSipCamera ) {
super()
this.sipCamera.binaryState = false
}
handle(request: SipRequest) {
//TODO: restrict this to call from:c300x@ AND to:alluser@ ?
if( request.method == 'CANCEL' ) {
let reason = request.headers["reason"] ? ( ' - ' + request.headers["reason"] ) : ''
this.sipCamera.console.log('CANCEL voice call from: ' + stringifyUri( request.headers.from.uri ) + ' to: ' + stringifyUri( request.headers.to.uri ) + reason )
this.sipCamera?.reset()
}
if( request.method === 'INVITE' ) {
this.sipCamera.console.log("INCOMING voice call from: " + stringifyUri( request.headers.from.uri ) + ' to: ' + stringifyUri( request.headers.to.uri ) )
this.sipCamera.binaryState = true
this.sipCamera.incomingCallRequest = request
setTimeout( () => {
// Assumption that flexisip only holds this call active for 20 seconds ... might be revised
this.sipCamera?.reset()
}, 20 * 1000 )
}
}
}

View File

@@ -0,0 +1,56 @@
import sdk, { ScryptedDeviceBase, Lock, LockState, HttpRequest, HttpResponse, HttpRequestHandler } from "@scrypted/sdk";
import { BticinoSipCamera } from "./bticino-camera";
export class BticinoSipLock extends ScryptedDeviceBase implements Lock, HttpRequestHandler {
private timeout : NodeJS.Timeout
constructor(public camera: BticinoSipCamera) {
super( camera.nativeId + "-lock")
}
lock(): Promise<void> {
if( !this.timeout ) {
this.timeout = setTimeout(() => {
this.lockState = LockState.Locked
this.timeout = undefined
} , 3000);
} else {
this.camera.console.log("Still attempting previous locking ...")
}
return
}
unlock(): Promise<void> {
this.lockState = LockState.Unlocked
this.lock()
return this.camera.sipUnlock()
}
public async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
if (request.url.endsWith('/unlocked')) {
this.lockState = LockState.Unlocked
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/locked') ) {
this.lockState = LockState.Locked
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/lock') ) {
this.lock();
response.send('Success', {
code: 200,
});
} else if( request.url.endsWith('/unlock') ) {
this.unlock();
response.send('Success', {
code: 200,
});
} else {
response.send('Unsupported operation', {
code: 400,
});
}
}
}

View File

@@ -0,0 +1,69 @@
import { SipRequestHandler, SipRequest } from "../../sip/src/sip-manager"
import { BticinoSipCamera } from "./bticino-camera"
export class VoicemailHandler extends SipRequestHandler {
private timeout : NodeJS.Timeout
constructor( private sipCamera : BticinoSipCamera ) {
super()
setTimeout( () => {
// Delay a bit an run in a different thread in case this fails
this.checkVoicemail()
}, 10000 )
}
checkVoicemail() {
if( !this.sipCamera )
return
if( this.isEnabled() ) {
this.sipCamera.console.debug("Checking answering machine, cameraId: " + this.sipCamera.id )
this.sipCamera.getAswmStatus().catch( e => this.sipCamera.console.error(e) )
} else {
this.sipCamera.console.debug("Answering machine check not enabled, cameraId: " + this.sipCamera.id )
}
//TODO: make interval customizable, now every 5 minutes
this.timeout = setTimeout( () => this.checkVoicemail() , 5 * 60 * 1000 )
}
cancelVoicemailCheck() {
if( this.timeout ) {
clearTimeout(this.timeout)
}
}
handle(request: SipRequest) {
if( this.isEnabled() ) {
const lastVoicemailMessageTimestamp : number = Number.parseInt( this.sipCamera.storage.getItem('lastVoicemailMessageTimestamp') ) || -1
const message : string = request.content.toString()
if( message.startsWith('*#8**40*0*0*1176*0*2##') ) {
this.sipCamera.console.debug("Handling incoming answering machine reply")
const messages : string[] = message.split(';')
let lastMessageTimestamp : number = 0
let countNewMessages : number = 0
messages.forEach( (message, index) => {
if( index > 0 ) {
const parts = message.split('|')
if( parts.length == 4 ) {
let messageTimestamp = Number.parseInt( parts[2] )
if( messageTimestamp > lastVoicemailMessageTimestamp )
countNewMessages++
if( index == messages.length-2 )
lastMessageTimestamp = messageTimestamp
}
}
} )
if( (lastVoicemailMessageTimestamp == null && lastMessageTimestamp > 0) ||
( lastVoicemailMessageTimestamp != null && lastMessageTimestamp > lastVoicemailMessageTimestamp ) ) {
this.sipCamera.log.a(`You have ${countNewMessages} new voicemail messages.`)
this.sipCamera.storage.setItem('lastVoicemailMessageTimestamp', lastMessageTimestamp.toString())
} else {
this.sipCamera.console.debug("No new messages since: " + lastVoicemailMessageTimestamp + " lastMessage: " + lastMessageTimestamp)
}
}
}
}
isEnabled() : boolean {
return this.sipCamera?.storage?.getItem('notifyVoicemail')?.toLocaleLowerCase() === 'true' || false
}
}

View File

@@ -1,379 +1,97 @@
import { listenZeroSingleClient } from '@scrypted/common/src/listen-cluster';
import { SipMessageHandler, SipCall, SipOptions, SipRequest } from '../../sip/src/sip-call';
import { RtspServer } from '@scrypted/common/src/rtsp-server';
import { addTrackControls, parseSdp, replacePorts } from '@scrypted/common/src/sdp-utils';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import sdk, { BinarySensor, Camera, DeviceCreator, DeviceCreatorSettings, DeviceProvider, FFmpegInput, Intercom, MediaObject, MediaStreamUrl, PictureOptions, ResponseMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera } from '@scrypted/sdk';
import { SipSession } from '../../sip/src/sip-session';
import { isStunMessage, getPayloadType, getSequenceNumber, isRtpMessagePayloadType } from '../../sip/src/rtp-utils';
import { randomBytes } from 'crypto';
const STREAM_TIMEOUT = 50000;
const SIP_EXPIRATION_DEFAULT = 3600;
const { deviceManager, mediaManager } = sdk;
export class SipCamera extends ScryptedDeviceBase implements Intercom, Camera, VideoCamera, Settings, BinarySensor {
session: SipSession;
currentMedia: FFmpegInput | MediaStreamUrl;
currentMediaMimeType: string;
refreshTimeout: NodeJS.Timeout;
messageHandler: SipMessageHandler;
constructor(nativeId: string, public provider: SipCamProvider) {
super(nativeId);
let logger = this.log;
this.messageHandler = new class extends SipMessageHandler {
handle( request: SipRequest ) {
// TODO: implement netatmo.onPresence handling?
// {"jsonrpc":"2.0","method":"netatmo.onPresence","params":[{"persons":[]}]}
logger.d("remote message: " + request.content );
}
}()
}
async takePicture(option?: PictureOptions): Promise<MediaObject> {
throw new Error("The SIP doorbell camera does not provide snapshots. Install the Snapshot Plugin if snapshots are available via an URL.");
}
async getPictureOptions(): Promise<PictureOptions[]> {
return;
}
settingsStorage = new StorageSettings(this, {
sipfrom: {
title: 'SIP From: URI',
type: 'string',
value: this.storage.getItem('sipfrom'),
description: 'SIP URI From field: Using the IP address of your server you will be calling from. Also the user and IP you added in /etc/flexisip/users/route_ext.conf on the intercom.',
placeholder: 'user@192.168.0.111',
multiple: false,
},
sipto: {
title: 'SIP To: URI',
type: 'string',
description: 'SIP URI To field: Must look like c300x@IP;transport=udp;rport and UDP transport is the only one supported right now.',
placeholder: 'c300x@192.168.0.2[:5060];transport=udp;rport',
},
sipdomain: {
title: 'SIP domain',
type: 'string',
description: 'SIP domain: The internal BTicino domain, usually has the following format: 2048362.bs.iotleg.com',
placeholder: '2048362.bs.iotleg.com',
},
sipexpiration: {
title: 'SIP UA expiration',
type: 'number',
range: [60, SIP_EXPIRATION_DEFAULT],
description: 'SIP UA expiration: How long the UA should remain active before expiring. Use 3600.',
placeholder: '3600',
},
sipdebug: {
title: 'SIP debug logging',
type: 'boolean',
description: 'Enable SIP debugging',
placeholder: 'true or false',
},
});
getSettings(): Promise<Setting[]> {
return this.settingsStorage.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.settingsStorage.putSetting(key, value);
}
async startIntercom(media: MediaObject): Promise<void> {
this.log.d( "TODO: startIntercom" + media );
}
async stopIntercom(): Promise<void> {
this.log.d( "TODO: stopIntercom" );
}
resetStreamTimeout() {
this.log.d('starting/refreshing stream');
clearTimeout(this.refreshTimeout);
this.refreshTimeout = setTimeout(() => this.stopSession(), STREAM_TIMEOUT);
}
stopSession() {
if (this.session) {
this.log.d('ending sip session');
this.session.stop();
this.session = undefined;
}
}
async getVideoStream(options?: ResponseMediaStreamOptions): Promise<MediaObject> {
if (options?.metadata?.refreshAt) {
if (!this.currentMedia?.mediaStreamOptions)
throw new Error("no stream to refresh");
const currentMedia = this.currentMedia;
currentMedia.mediaStreamOptions.refreshAt = Date.now() + STREAM_TIMEOUT;
currentMedia.mediaStreamOptions.metadata = {
refreshAt: currentMedia.mediaStreamOptions.refreshAt
};
this.resetStreamTimeout();
return mediaManager.createMediaObject(currentMedia, this.currentMediaMimeType);
}
this.stopSession();
const { clientPromise: playbackPromise, port: playbackPort, url: clientUrl } = await listenZeroSingleClient();
const playbackUrl = `rtsp://127.0.0.1:${playbackPort}`;
playbackPromise.then(async (client) => {
client.setKeepAlive(true, 10000);
let sip: SipSession;
try {
let rtsp: RtspServer;
const cleanup = () => {
client.destroy();
if (this.session === sip)
this.session = undefined;
try {
this.log.d('cleanup(): stopping sip session.');
sip.stop();
}
catch (e) {
}
rtsp?.destroy();
}
client.on('close', cleanup);
client.on('error', cleanup);
const from = this.storage.getItem('sipfrom')?.trim();
const to = this.storage.getItem('sipto')?.trim();
const localIp = from?.split(':')[0].split('@')[1];
const localPort = parseInt(from?.split(':')[1]) || 5060;
const domain = this.storage.getItem('sipdomain')?.trim();
const expiration : string = this.storage.getItem('sipuaexpiration')?.trim() || '3600';
const sipdebug : boolean = this.storage.getItem('sipdebug')?.toLocaleLowerCase() === 'true' || false;
if (!from || !to || !localIp || !localPort || !domain || !expiration ) {
this.log.e('Error: SIP From/To/Domain URIs not specified!');
return;
}
//TODO settings
let sipOptions : SipOptions = {
from: "sip:" + from,
to: "sip:" + to,
domain: domain,
expire: Number.parseInt( expiration ),
localIp,
localPort,
shouldRegister: true,
debugSip: sipdebug,
messageHandler: this.messageHandler
};
sip = await SipSession.createSipSession(console, "Bticino", sipOptions);
sip.onCallEnded.subscribe(cleanup);
// Call the C300X
let remoteRtpDescription = await sip.call(
( audio ) => {
return [
'a=DEVADDR:20', // Needed for bt_answering_machine (bticino specific)
`m=audio ${audio.port} RTP/SAVP 97`,
`a=rtpmap:97 speex/8000`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:/qE7OPGKp9hVGALG2KcvKWyFEZfSSvm7bYVDjT8X`,
]
}, ( video ) => {
return [
`m=video ${video.port} RTP/SAVP 97`,
`a=rtpmap:97 H264/90000`,
`a=fmtp:97 profile-level-id=42801F`,
`a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:/qE7OPGKp9hVGALG2KcvKWyFEZfSSvm7bYVDjT8X`,
'a=recvonly'
]
} );
if( sipOptions.debugSip )
this.log.d('SIP: Received remote SDP:\n' + remoteRtpDescription.sdp)
let sdp: string = replacePorts( remoteRtpDescription.sdp, 0, 0 );
sdp = addTrackControls(sdp);
sdp = sdp.split('\n').filter(line => !line.includes('a=rtcp-mux')).join('\n');
if( sipOptions.debugSip )
this.log.d('SIP: Updated SDP:\n' + sdp);
let vseq = 0;
let vseen = 0;
let vlost = 0;
let aseq = 0;
let aseen = 0;
let alost = 0;
rtsp = new RtspServer(client, sdp, true);
const parsedSdp = parseSdp(rtsp.sdp);
const videoTrack = parsedSdp.msections.find(msection => msection.type === 'video').control;
const audioTrack = parsedSdp.msections.find(msection => msection.type === 'audio').control;
if( sipOptions.debugSip ) {
rtsp.console = this.console;
}
await rtsp.handlePlayback();
sip.videoSplitter.on('message', message => {
if (!isStunMessage(message)) {
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
if (!isRtpMessage)
return;
vseen++;
rtsp.sendTrack(videoTrack, message, !isRtpMessage);
const seq = getSequenceNumber(message);
if (seq !== (vseq + 1) % 0x0FFFF)
vlost++;
vseq = seq;
}
});
sip.videoRtcpSplitter.on('message', message => {
rtsp.sendTrack(videoTrack, message, true);
});
sip.audioSplitter.on('message', message => {
if (!isStunMessage(message)) {
const isRtpMessage = isRtpMessagePayloadType(getPayloadType(message));
if (!isRtpMessage)
return;
aseen++;
rtsp.sendTrack(audioTrack, message, !isRtpMessage);
const seq = getSequenceNumber(message);
if (seq !== (aseq + 1) % 0x0FFFF)
alost++;
aseq = seq;
}
});
sip.audioRtcpSplitter.on('message', message => {
rtsp.sendTrack(audioTrack, message, true);
});
this.session = sip;
try {
await rtsp.handleTeardown();
this.log.d('rtsp client ended');
}
catch (e) {
this.log.e('rtsp client ended ungracefully' + e);
}
finally {
cleanup();
}
}
catch (e) {
sip?.stop();
throw e;
}
});
this.resetStreamTimeout();
const mediaStreamOptions = Object.assign(this.getSipMediaStreamOptions(), {
refreshAt: Date.now() + STREAM_TIMEOUT,
});
const mediaStreamUrl: MediaStreamUrl = {
url: playbackUrl,
mediaStreamOptions,
};
this.currentMedia = mediaStreamUrl;
this.currentMediaMimeType = ScryptedMimeTypes.MediaStreamUrl;
return mediaManager.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl);
}
getSipMediaStreamOptions(): ResponseMediaStreamOptions {
return {
id: 'sip',
name: 'SIP',
// this stream is NOT scrypted blessed due to wackiness in the h264 stream.
// tool: "scrypted",
container: 'sdp',
audio: {
// this is a hint to let homekit, et al, know that it's speex audio and needs transcoding.
codec: 'speex',
},
source: 'local',
userConfigurable: false,
};
}
async getVideoStreamOptions(): Promise<ResponseMediaStreamOptions[]> {
return [
this.getSipMediaStreamOptions(),
]
}
}
export class SipCamProvider extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
devices = new Map<string, any>();
constructor(nativeId?: string) {
super(nativeId);
for (const camId of deviceManager.getNativeIds()) {
if (camId)
this.getDevice(camId);
}
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const nativeId = randomBytes(4).toString('hex');
const name = settings.newCamera.toString();
await this.updateDevice(nativeId, name);
return nativeId;
}
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
key: 'newCamera',
title: 'Add Camera',
placeholder: 'Camera name, e.g.: Back Yard Camera, Baby Camera, etc',
}
]
}
updateDevice(nativeId: string, name: string) {
return deviceManager.onDeviceDiscovered({
nativeId,
name,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.VideoCamera,
ScryptedInterface.Settings,
ScryptedInterface.Intercom,
ScryptedInterface.BinarySensor
],
type: ScryptedDeviceType.Doorbell,
});
}
getDevice(nativeId: string) {
let ret = this.devices.get(nativeId);
if (!ret) {
ret = this.createCamera(nativeId);
if (ret)
this.devices.set(nativeId, ret);
}
return ret;
}
createCamera(nativeId: string): SipCamera {
return new SipCamera(nativeId, this);
}
}
export default new SipCamProvider();
import sdk, { Device, DeviceCreator, DeviceCreatorSettings, DeviceProvider, LockState, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting } from '@scrypted/sdk'
import { randomBytes } from 'crypto'
import { BticinoSipCamera } from './bticino-camera'
const { systemManager, deviceManager } = sdk
export class BticinoSipPlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
devices = new Map<string, BticinoSipCamera>()
async getCreateDeviceSettings(): Promise<Setting[]> {
return [
{
key: 'newCamera',
title: 'Add Camera',
placeholder: 'Camera name, e.g.: Back Yard Camera, Baby Camera, etc',
}
]
}
async createDevice(settings: DeviceCreatorSettings): Promise<string> {
const nativeId = randomBytes(4).toString('hex')
const name = settings.newCamera?.toString()
const camera = await this.updateDevice(nativeId, name)
const device: Device = {
providerNativeId: nativeId,
info: {
//model: `${camera.model} (${camera.data.kind})`,
manufacturer: 'BticinoPlugin',
//firmware: camera.data.firmware_version,
//serialNumber: camera.data.device_id
},
nativeId: nativeId + '-lock',
name: name + ' Lock',
type: ScryptedDeviceType.Lock,
interfaces: [ScryptedInterface.Lock, ScryptedInterface.HttpRequestHandler],
}
const ret = await deviceManager.onDevicesChanged({
providerNativeId: nativeId,
devices: [device],
})
let sipCamera : BticinoSipCamera = await this.getDevice(nativeId)
let foo : BticinoSipCamera = systemManager.getDeviceById<BticinoSipCamera>(sipCamera.id)
let lock = await sipCamera.getDevice(undefined)
lock.lockState = LockState.Locked
return nativeId
}
updateDevice(nativeId: string, name: string) {
return deviceManager.onDeviceDiscovered({
nativeId,
info: {
//model: `${camera.model} (${camera.data.kind})`,
manufacturer: 'BticinoSipPlugin',
//firmware: camera.data.firmware_version,
//serialNumber: camera.data.device_id
},
name,
interfaces: [
ScryptedInterface.Camera,
ScryptedInterface.VideoCamera,
ScryptedInterface.Settings,
ScryptedInterface.Intercom,
ScryptedInterface.BinarySensor,
ScryptedDeviceType.DeviceProvider,
ScryptedInterface.HttpRequestHandler,
ScryptedInterface.VideoClips
],
type: ScryptedDeviceType.Doorbell,
})
}
async getDevice(nativeId: string): Promise<any> {
if (!this.devices.has(nativeId)) {
const camera = new BticinoSipCamera(nativeId, this)
this.devices.set(nativeId, camera)
}
return this.devices.get(nativeId)
}
async releaseDevice(id: string, nativeId: string): Promise<void> {
let camera = this.devices.get(nativeId)
if( camera ) {
camera.voicemailHandler.cancelVoicemailCheck()
if( this.devices.delete( nativeId ) ) {
this.console.log("Removed device from list: " + id + " / " + nativeId )
}
}
}
}
export default new BticinoSipPlugin()

View File

@@ -0,0 +1,71 @@
import { SipCallSession } from "../../sip/src/sip-call-session";
import { BticinoSipCamera } from "./bticino-camera";
import { SipHelper } from "./sip-helper";
import { SipManager, SipOptions } from "../../sip/src/sip-manager";
/**
* This class registers itself with the SIP server as a contact for a user account.
* The registration expires after the expires time in sipOptions is reached.
* The sip session will re-register itself after the expires time is reached.
*/
const CHECK_INTERVAL : number = 10 * 1000
export class PersistentSipManager {
private sipManager : SipManager
private lastRegistration : number = 0
private expireInterval : number = 0
constructor( private camera : BticinoSipCamera ) {
// Give it a second and run in seperate thread to avoid failure on creation for from/to/domain check
setTimeout( () => this.enable() , CHECK_INTERVAL )
}
async enable() : Promise<SipManager> {
if( this.sipManager ) {
return this.sipManager
} else {
return this.register()
}
}
private async register() : Promise<SipManager> {
let now = Date.now()
try {
let sipOptions : SipOptions = SipHelper.sipOptions( this.camera )
if( Number.isNaN( sipOptions.expire ) || sipOptions.expire <= 0 || sipOptions.expire > 3600 ) {
sipOptions.expire = 300
}
if( this.expireInterval == 0 ) {
this.expireInterval = (sipOptions.expire * 1000) - 10000
}
if( !this.camera.hasActiveCall() && now - this.lastRegistration >= this.expireInterval ) {
let sipOptions : SipOptions = SipHelper.sipOptions( this.camera )
this.sipManager?.destroy()
this.sipManager = new SipManager(this.camera.console, sipOptions )
await this.sipManager.register()
this.lastRegistration = now
return this.sipManager;
}
} catch(e) {
this.camera.console.error("Error enabling persistent SIP manager: " + e )
// Try again in a minute
this.lastRegistration = now + (60 * 1000) - this.expireInterval
throw e
} finally {
setTimeout( () => this.register(), CHECK_INTERVAL )
}
}
async session( sipOptions: SipOptions ) : Promise<SipCallSession> {
let sm = await this.enable()
return SipCallSession.createCallSession(this.camera.console, "Bticino", sipOptions, sm )
}
reloadSipOptions() {
this.sipManager?.setSipOptions( null )
}
}

View File

@@ -0,0 +1,59 @@
import { SipOptions } from "../../sip/src/sip-manager";
import { BticinoSipCamera } from "./bticino-camera";
import crypto from 'crypto';
export class SipHelper {
public static sipOptions( camera : BticinoSipCamera ) : SipOptions {
// Might be removed soon?
if( camera.storage.getItem('sipto') && camera.storage.getItem('sipto').toString().indexOf(';') > 0 ) {
camera.storage.setItem('sipto', camera.storage.getItem('sipto').toString().split(';')[0] )
}
const from = camera.storage.getItem('sipfrom')?.trim()
const to = camera.storage.getItem('sipto')?.trim()
const localIp = from?.split(':')[0].split('@')[1]
// Although this might not occur directly, each camera should run on its own port
// Might need to use a random free port here (?)
const localPort = parseInt(from?.split(':')[1]) || 5060
const domain = camera.storage.getItem('sipdomain')?.trim()
const expiration : string = camera.storage.getItem('sipexpiration')?.trim() || '600'
const sipdebug : boolean = camera.storage.getItem('sipdebug')?.toLocaleLowerCase() === 'true' || false
if (!from || !to || !localIp || !localPort || !domain || !expiration ) {
camera.log.e('Error: SIP From/To/Domain URIs not specified!')
throw new Error('SIP From/To/Domain URIs not specified!')
}
return {
from: "sip:" + from,
//TCP is more reliable for large messages, also see useTcp=true below
to: "sip:" + to + ";transport=tcp",
domain: domain,
expire: Number.parseInt( expiration ),
localIp,
localPort,
debugSip: sipdebug,
gruuInstanceId: SipHelper.getGruuInstanceId(camera),
useTcp: true,
sipRequestHandler: camera.requestHandlers
}
}
public static getIntercomIp( camera : BticinoSipCamera ): string {
let to = camera.storage.getItem('sipto')?.trim();
if( to ) {
return to.split('@')[1];
}
return
}
public static getGruuInstanceId( camera : BticinoSipCamera ): string {
let md5 = camera.storage.getItem('md5hash')
if( !md5 ) {
md5 = crypto.createHash('md5').update( camera.nativeId ).digest("hex")
md5 = md5.substring(0, 8) + '-' + md5.substring(8, 12) + '-' + md5.substring(12,16) + '-' + md5.substring(16, 32)
camera.storage.setItem('md5has', md5)
}
return md5
}
}

View File

@@ -0,0 +1,78 @@
import { Setting, SettingValue } from '@scrypted/sdk';
import { StorageSettings } from '@scrypted/sdk/storage-settings';
import { BticinoSipCamera } from './bticino-camera';
export class BticinoStorageSettings {
private storageSettings
constructor(camera : BticinoSipCamera) {
this.storageSettings = new StorageSettings( camera, {
sipfrom: {
title: 'SIP From: URI',
type: 'string',
value: camera.storage.getItem('sipfrom'),
description: 'SIP URI From field: Using the IP address of your server you will be calling from.',
placeholder: 'user@192.168.0.111',
multiple: false,
},
sipto: {
title: 'SIP To: URI',
type: 'string',
description: 'SIP URI To field: Must look like c300x@192.168.0.2',
placeholder: 'c300x@192.168.0.2',
},
sipdomain: {
title: 'SIP domain',
type: 'string',
description: 'SIP domain - tshe internal BTicino domain, usually has the following format: 2048362.bs.iotleg.com',
placeholder: '2048362.bs.iotleg.com',
},
sipexpiration: {
title: 'SIP UA expiration',
type: 'number',
range: [60, 3600],
description: 'How long the UA should remain active before expiring and having to re-register (in seconds)',
defaultValue: 600,
placeholder: '600',
},
sipdebug: {
title: 'SIP debug logging',
type: 'boolean',
description: 'Enable SIP debugging',
placeholder: 'true or false',
},
notifyVoicemail: {
title: 'Notify on new voicemail messages',
type: 'boolean',
description: 'Enable voicemail alerts',
placeholder: 'true or false',
},
doorbellWebhookUrl: {
title: 'Doorbell Sensor Webhook',
type: 'string',
readonly: true,
mapGet: () => {
return camera.doorbellWebhookUrl;
},
description: 'Incoming doorbell sensor webhook url.',
},
doorbellLockWebhookUrl: {
title: 'Doorbell Lock Webhook',
type: 'string',
readonly: true,
mapGet: () => {
return camera.doorbellLockWebhookUrl;
},
description: 'Incoming doorbell sensor webhook url.',
}
});
}
getSettings(): Promise<Setting[]> {
return this.storageSettings.getSettings();
}
putSetting(key: string, value: SettingValue): Promise<void> {
return this.storageSettings.putSetting(key, value);
}
}

View File

@@ -88,7 +88,10 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
}
client.removeAllListeners();
client.close();
try {
client.close();
} catch (e) {
}
}
client.client.on('close', cleanup);
client.on('error', err => {
@@ -149,6 +152,14 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
}
async load(media: string | MediaObject, options: MediaPlayerOptions) {
if (this.mediaPlayerPromise) {
try {
(await this.mediaPlayerPromise).close();
} catch (e) {
}
this.mediaPlayerPromise = undefined;
this.mediaPlayerStatus = undefined;
}
let url: string;
let urlMimeType: string;
@@ -341,15 +352,7 @@ class CastDevice extends ScryptedDeviceBase implements MediaPlayer, Refresh, Eng
});
})
player.getStatus((err, status) => {
if (err) {
reject(err);
return;
}
this.mediaPlayerStatus = status;
this.updateState();
resolve(player);
})
resolve(player);
});
});
});

View File

@@ -1,26 +1,28 @@
{
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.14",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/cloud",
"version": "0.1.11",
"version": "0.1.14",
"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.97",
"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.14"
}

View File

@@ -1,23 +1,22 @@
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 upnp from 'nat-upnp';
import net 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;
@@ -210,6 +209,11 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
})
this.updateCors();
if (!this.storageSettings.values.token_info && process.env.SCRYPTED_CLOUD_TOKEN) {
this.storageSettings.values.token_info = process.env.SCRYPTED_CLOUD_TOKEN;
this.manager.registrationId.then(r => this.sendRegistrationId(r));
}
}
scheduleRefreshPortForward() {
@@ -547,6 +551,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 +626,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

@@ -29,7 +29,7 @@ class ChromecastViewCameraExample implements StartStop {
}
async stop() {
device.running = false;
return chromecast.stop();
await chromecast.stop();
}
}

View File

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

View File

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

View File

@@ -27,32 +27,14 @@ export class Scheduler {
];
const date = new Date();
if (schedule.clockType === 'AM' || schedule.clockType === 'PM') {
let hour = schedule.hour;
if (schedule.clockType === 'AM') {
if (hour === 12)
hour -= 12;
}
else {
if (hour != 12)
hour += 12;
}
date.setHours(hour);
date.setMinutes(schedule.minute, 0, 0);
}
else if (schedule.clockType === '24HourClock') {
date.setHours(schedule.hour, schedule.minute, 0, 0);
}
else {
throw new Error('sunrise/sunset clock not supported');
}
date.setHours(schedule.hour);
date.setMinutes(schedule.minute);
const ret: ScryptedDevice = {
async setName() { },
async setType() { },
async setRoom() { },
async setMixins() { },
async probe() { return true },
listen(event: EventListenerOptions, callback, source?: ScryptedDeviceBase) {
function reschedule(): Date {
@@ -66,7 +48,7 @@ export class Scheduler {
if (!days[day])
continue;
source.log.i(`event will fire at ${future}`);
source.log.i(`event will fire at ${future.toLocaleString()}`);
return future;
}
source.log.w('event will never fire');
@@ -81,6 +63,7 @@ export class Scheduler {
}
const delay = when.getTime() - Date.now();
source.log.i(`event will fire in ${Math.round(delay / 60 / 1000)} minutes.`);
let timeout = setTimeout(() => {
reschedule();

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';
@@ -16,7 +17,13 @@ const { systemManager, deviceManager, endpointManager } = sdk;
const indexHtml = fs.readFileSync('dist/index.html').toString();
export function getAddresses() {
const addresses = Object.entries(os.networkInterfaces()).filter(([iface]) => iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')).map(([_, addr]) => addr).flat().map(info => info.address).filter(address => address);
const addresses: string[] = [];
for (const [iface, nif] of Object.entries(os.networkInterfaces())) {
if (iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')) {
addresses.push(iface);
addresses.push(...nif.map(addr => addr.address));
}
}
return addresses;
}
@@ -36,17 +43,18 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
localAddresses: string[];
storageSettings = new StorageSettings(this, {
localAddresses: {
title: 'Scrypted Server Address',
description: 'The IP address used by the Scrypted server. Set this to the wired IP address to prevent usage of a wireless address.',
title: 'Scrypted Server Addresses',
description: 'The IP addresses used by the Scrypted server. Set this to the wired IP address to prevent usage of a wireless address.',
combobox: true,
multiple: true,
async onGet() {
return {
choices: getAddresses(),
};
},
mapGet: () => this.localAddresses?.[0],
mapGet: () => this.localAddresses,
onPut: async (oldValue, newValue) => {
this.localAddresses = newValue ? [newValue] : undefined;
this.localAddresses = newValue?.length ? newValue : undefined;
const service = await sdk.systemManager.getComponent('addresses');
service.setLocalAddresses(this.localAddresses);
},
@@ -91,6 +99,17 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
this.automationCore = new AutomationCore();
})();
deviceManager.onDeviceDiscovered({
name: 'Add to Launcher',
nativeId: 'launcher',
interfaces: [
'@scrypted/launcher-ignore',
ScryptedInterface.MixinProvider,
ScryptedInterface.Readme,
],
type: ScryptedDeviceType.Builtin,
});
(async () => {
await deviceManager.onDeviceDiscovered(
{
@@ -120,7 +139,7 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
async getSettings(): Promise<Setting[]> {
try {
const service = await sdk.systemManager.getComponent('addresses');
this.localAddresses = await service.getLocalAddresses();
this.localAddresses = await service.getLocalAddresses(true);
}
catch (e) {
}
@@ -237,3 +256,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,42 +3,58 @@ 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: [],
},
})
async getScryptedUserAccessControl(): Promise<ScryptedUserAccessControl> {
const usersService = await sdk.systemManager.getComponent('users');
const users: DBUser[] = await usersService.getAllUsers();
const user = users.find(user => user.username === this.username);
if (!user)
throw new Error("user not found");
if (user.admin)
return;
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 +88,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

@@ -13,7 +13,6 @@
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/vue-fontawesome": "^2.0.8",
"@radial-color-picker/vue-color-picker": "^2.3.0",
"@scrypted/client": "file:../../../packages/client",
"@scrypted/common": "file:../../../common",
"@scrypted/sdk": "file:../../../sdk",
"@scrypted/types": "file:../../../sdk/types",
@@ -32,6 +31,7 @@
"register-service-worker": "^1.7.2",
"router": "^1.3.6",
"semver": "^6.3.0",
"v-calendar": "^2.4.1",
"vue": "^2.7.14",
"vue-apexcharts": "^1.6.2",
"vue-async-computed": "^3.9.0",
@@ -118,27 +118,24 @@
},
"../../../packages/client": {
"name": "@scrypted/client",
"version": "1.1.37",
"version": "1.1.48",
"extraneous": true,
"license": "ISC",
"dependencies": {
"@scrypted/types": "^0.2.64",
"adm-zip": "^0.5.9",
"@scrypted/types": "^0.2.78",
"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"
}
},
"../../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.68",
"version": "0.2.87",
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
@@ -175,7 +172,7 @@
},
"../../../sdk/types": {
"name": "@scrypted/types",
"version": "0.2.63",
"version": "0.2.79",
"license": "ISC",
"devDependencies": {
"@types/rimraf": "^3.0.2",
@@ -2265,6 +2262,16 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"node_modules/@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@radial-color-picker/color-wheel": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@radial-color-picker/color-wheel/-/color-wheel-2.2.0.tgz",
@@ -2287,10 +2294,6 @@
"vue": "^2.5.21"
}
},
"node_modules/@scrypted/client": {
"resolved": "../../../packages/client",
"link": true
},
"node_modules/@scrypted/common": {
"resolved": "../../../common",
"link": true
@@ -7819,7 +7822,6 @@
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
"dev": true,
"engines": {
"node": ">=0.11"
},
@@ -7828,6 +7830,14 @@
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/date-fns-tz": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
"peerDependencies": {
"date-fns": ">=2.0.0"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -18977,6 +18987,31 @@
"uuid": "bin/uuid"
}
},
"node_modules/v-calendar": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-2.4.1.tgz",
"integrity": "sha512-nhzOlHM2cinv+8jIcnAx+nTo63U40szv3Ig41uLMpGK1U5sApgCP6ggigprsnlMOM5VRq1G/1B8rNHkRrLbGjw==",
"dependencies": {
"core-js": "^3.15.2",
"date-fns": "^2.22.1",
"date-fns-tz": "^1.1.4",
"lodash": "^4.17.21"
},
"peerDependencies": {
"@popperjs/core": "^2.4.0",
"vue": "^2.5.18"
}
},
"node_modules/v-calendar/node_modules/core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
@@ -22773,6 +22808,12 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"peer": true
},
"@radial-color-picker/color-wheel": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@radial-color-picker/color-wheel/-/color-wheel-2.2.0.tgz",
@@ -22792,22 +22833,6 @@
"@radial-color-picker/rotator": "2.1.0"
}
},
"@scrypted/client": {
"version": "file:../../../packages/client",
"requires": {
"@scrypted/types": "^0.2.64",
"@types/adm-zip": "^0.4.34",
"@types/ip": "^1.1.0",
"@types/node": "^17.0.17",
"adm-zip": "^0.5.9",
"axios": "^0.25.0",
"engine.io-client": "^6.2.2",
"linkfs": "^2.1.0",
"memfs": "^3.4.1",
"rimraf": "^3.0.2",
"typescript": "^4.7.4"
}
},
"@scrypted/common": {
"version": "file:../../../common",
"requires": {
@@ -27308,8 +27333,13 @@
"date-fns": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
"dev": true
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw=="
},
"date-fns-tz": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz",
"integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==",
"requires": {}
},
"de-indent": {
"version": "1.0.2",
@@ -36063,6 +36093,24 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
},
"v-calendar": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/v-calendar/-/v-calendar-2.4.1.tgz",
"integrity": "sha512-nhzOlHM2cinv+8jIcnAx+nTo63U40szv3Ig41uLMpGK1U5sApgCP6ggigprsnlMOM5VRq1G/1B8rNHkRrLbGjw==",
"requires": {
"core-js": "^3.15.2",
"date-fns": "^2.22.1",
"date-fns-tz": "^1.1.4",
"lodash": "^4.17.21"
},
"dependencies": {
"core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
}
}
},
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",

View File

@@ -34,6 +34,7 @@
"register-service-worker": "^1.7.2",
"router": "^1.3.6",
"semver": "^6.3.0",
"v-calendar": "^2.4.1",
"vue": "^2.7.14",
"vue-apexcharts": "^1.6.2",
"vue-async-computed": "^3.9.0",

View File

@@ -1,3 +1,4 @@
import { timeoutPromise } from "@scrypted/common/src/promise-utils";
import { MixinProvider, ScryptedDevice, ScryptedDeviceType, ScryptedInterface, SystemManager } from "@scrypted/types";
export async function setMixin(systemManager: SystemManager, device: ScryptedDevice, mixinId: string, enabled: boolean) {
@@ -14,19 +15,21 @@ export async function setMixin(systemManager: SystemManager, device: ScryptedDev
plugins.setMixins(device.id, mixins);
}
export function getAllDevices(systemManager: SystemManager) {
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById(id)).filter(device => !!device);
export function getAllDevices<T>(systemManager: SystemManager) {
return Object.keys(systemManager.getSystemState()).map(id => systemManager.getDeviceById(id) as T & ScryptedDevice).filter(device => !!device);
}
export async function getDeviceAvailableMixins(systemManager: SystemManager, device: ScryptedDevice): Promise<(ScryptedDevice & MixinProvider)[]> {
const results = await Promise.all(getAllDevices(systemManager).map(async (check) => {
const results = await Promise.all(getAllDevices<MixinProvider>(systemManager).map(async (check) => {
try {
if (check.interfaces.includes(ScryptedInterface.MixinProvider)) {
if (await (check as any as MixinProvider).canMixin(device.type, device.interfaces))
return check as MixinProvider & ScryptedDevice;
const canMixin = await timeoutPromise(5000, check.canMixin(device.type, device.interfaces));
if (canMixin)
return check;
}
}
catch (e) {
console.warn(check.name, 'canMixin error', e)
}
}));
@@ -47,7 +50,7 @@ export async function getMixinProviderAvailableDevices(systemManager: SystemMana
devices.map(async (device) => {
try {
if (device.mixins?.includes(mixinProvider.id) || (await mixinProvider.canMixin(device.type, device.interfaces)))
return device;
return device;
}
catch (e) {
}

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