mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 22:23:27 +00:00
Compare commits
578 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a05595ecc7 | ||
|
|
87be4648f1 | ||
|
|
60e51adb41 | ||
|
|
ace7720fe1 | ||
|
|
b9eb74d403 | ||
|
|
fb7353383d | ||
|
|
bee119b486 | ||
|
|
0b6ffc2b87 | ||
|
|
3863527b4d | ||
|
|
51c48f4a1c | ||
|
|
4c138e9b4c | ||
|
|
e762c305a3 | ||
|
|
5bce335288 | ||
|
|
8201e9883a | ||
|
|
74e5884285 | ||
|
|
9cffd9ffbe | ||
|
|
d8b617f2ae | ||
|
|
aeb564aa5d | ||
|
|
45f672883a | ||
|
|
c0ff857a1b | ||
|
|
64f7e31f54 | ||
|
|
6b55f8876e | ||
|
|
718a31f2c5 | ||
|
|
c1e1d50fa5 | ||
|
|
75c4a1939f | ||
|
|
0d703c2aff | ||
|
|
0a6e4fda75 | ||
|
|
4c2de9e443 | ||
|
|
b8a4fedf1a | ||
|
|
79d9f1d4a1 | ||
|
|
983213c578 | ||
|
|
7dd3d71ebd | ||
|
|
493f8deeef | ||
|
|
b29f2d5ee1 | ||
|
|
96bda10123 | ||
|
|
3294700d31 | ||
|
|
0cf77d4c76 | ||
|
|
953841e3a5 | ||
|
|
393c1017df | ||
|
|
f50176d14a | ||
|
|
7f2bf0b542 | ||
|
|
9e3990400c | ||
|
|
95eed80735 | ||
|
|
be43d0c017 | ||
|
|
386ea9a98a | ||
|
|
9b40978f61 | ||
|
|
f0ee435cd0 | ||
|
|
30748784ef | ||
|
|
8310e33719 | ||
|
|
1d18697161 | ||
|
|
d500b3fd6c | ||
|
|
95ae916b6c | ||
|
|
ec3e16f20f | ||
|
|
30d28f543c | ||
|
|
e0cce24999 | ||
|
|
409b25f8b0 | ||
|
|
8f278abec8 | ||
|
|
d6179dab82 | ||
|
|
ed186e2142 | ||
|
|
3c021bb2c8 | ||
|
|
c522edc622 | ||
|
|
022a103bcb | ||
|
|
efd125b6e4 | ||
|
|
19f7688a65 | ||
|
|
7f18e4629c | ||
|
|
dfe2c937a1 | ||
|
|
47d7a23a3d | ||
|
|
0ea609c80c | ||
|
|
71ee5727f1 | ||
|
|
2383f16112 | ||
|
|
7d5defd736 | ||
|
|
cbf4cf0579 | ||
|
|
422dd94e5c | ||
|
|
076f5e27f1 | ||
|
|
645de2e5fd | ||
|
|
dcf24a77d7 | ||
|
|
7065365a47 | ||
|
|
b82520776e | ||
|
|
638c1f77fd | ||
|
|
73a489ea37 | ||
|
|
77d69f025a | ||
|
|
3bc14ad248 | ||
|
|
03e5a9dec1 | ||
|
|
57b790c332 | ||
|
|
ce2ea63be7 | ||
|
|
2dd4721b7f | ||
|
|
667075dfad | ||
|
|
7abdb06b66 | ||
|
|
43e5822c93 | ||
|
|
bc579514e7 | ||
|
|
825100f94e | ||
|
|
803bfc1560 | ||
|
|
b2013a54ed | ||
|
|
f252407935 | ||
|
|
516f2a2a7b | ||
|
|
c1677ce691 | ||
|
|
5028fb812d | ||
|
|
2db4e2579f | ||
|
|
b339ca6cd2 | ||
|
|
f100999cb1 | ||
|
|
2863756bd6 | ||
|
|
cc408850a0 | ||
|
|
ed1ceeda51 | ||
|
|
df09d8e92a | ||
|
|
298ac960d1 | ||
|
|
62d4d55aae | ||
|
|
a2121c0dc5 | ||
|
|
9b5ea27c0b | ||
|
|
0b0e90fc04 | ||
|
|
d8aff609bf | ||
|
|
d8283c261a | ||
|
|
e3aca964be | ||
|
|
a96025c45f | ||
|
|
6afd4b4579 | ||
|
|
f97669949d | ||
|
|
0a0a31574f | ||
|
|
90fb751a22 | ||
|
|
b8d06fada5 | ||
|
|
2cecb1686f | ||
|
|
db03775530 | ||
|
|
cccbc33f1a | ||
|
|
5f23873366 | ||
|
|
e43accae67 | ||
|
|
b3a0cda6f9 | ||
|
|
58c3348282 | ||
|
|
a9e6d76e99 | ||
|
|
3b58936387 | ||
|
|
3a14ab81c8 | ||
|
|
291178a7b5 | ||
|
|
b65faf1a79 | ||
|
|
9d8a1353c0 | ||
|
|
b29d793178 | ||
|
|
d8e406d415 | ||
|
|
4529872fd6 | ||
|
|
fa86c31340 | ||
|
|
94ded75d40 | ||
|
|
887b61cd7a | ||
|
|
48e3d30987 | ||
|
|
02dba3cd71 | ||
|
|
195769034d | ||
|
|
39c08aa378 | ||
|
|
fa8056d38e | ||
|
|
145f116c68 | ||
|
|
15b6f336e4 | ||
|
|
8b46f0a466 | ||
|
|
a20cc5cd89 | ||
|
|
3d068929fd | ||
|
|
928f9b7579 | ||
|
|
c1c5a42645 | ||
|
|
12643cdde2 | ||
|
|
0bff96a6e6 | ||
|
|
4e7e67de54 | ||
|
|
65c4a30004 | ||
|
|
309a1dc11f | ||
|
|
b7904b73b2 | ||
|
|
9e9ddbc5f3 | ||
|
|
ceda54f91b | ||
|
|
1d4052b839 | ||
|
|
6a5d6e6617 | ||
|
|
f55cc6066f | ||
|
|
527714e434 | ||
|
|
8a1633ffa3 | ||
|
|
56b2ab9c4f | ||
|
|
d330e2eb9d | ||
|
|
b55e7cacb3 | ||
|
|
c70375db06 | ||
|
|
2c23021d40 | ||
|
|
84a4ef4539 | ||
|
|
7f3db0549b | ||
|
|
de0e1784a3 | ||
|
|
5a8798638e | ||
|
|
14da49728c | ||
|
|
55423b2d09 | ||
|
|
596106247b | ||
|
|
5472d90368 | ||
|
|
fcf58413fc | ||
|
|
0d03b91753 | ||
|
|
2fd088e4d6 | ||
|
|
c6933198b2 | ||
|
|
210e684a22 | ||
|
|
53cc4b6ef3 | ||
|
|
d58d138a68 | ||
|
|
c0199a2b76 | ||
|
|
badb1905ce | ||
|
|
735c2dce7b | ||
|
|
ffae3f246f | ||
|
|
31b424f89f | ||
|
|
3b7acc3a90 | ||
|
|
7e66d1ac7f | ||
|
|
a613da069e | ||
|
|
40b73c6589 | ||
|
|
ef16ca83a2 | ||
|
|
76bf1d0d3f | ||
|
|
3d5ccf25d1 | ||
|
|
36fcb713d9 | ||
|
|
e306631850 | ||
|
|
17400fa886 | ||
|
|
c6dc628616 | ||
|
|
f974653e73 | ||
|
|
b83880a8a3 | ||
|
|
ee4d8f52df | ||
|
|
3854b75c6e | ||
|
|
07c3173506 | ||
|
|
2894ab1b96 | ||
|
|
99995ea882 | ||
|
|
d6560fbbe4 | ||
|
|
7205583104 | ||
|
|
8479a16d3d | ||
|
|
409aad4794 | ||
|
|
a29d009e5c | ||
|
|
6772419ccf | ||
|
|
38746ee743 | ||
|
|
c5cb3ffa90 | ||
|
|
e119056267 | ||
|
|
590ad3de37 | ||
|
|
6cd412de88 | ||
|
|
33ca0242b1 | ||
|
|
68d3f10888 | ||
|
|
7a844aac84 | ||
|
|
6f2bb9fd9e | ||
|
|
12e47993a4 | ||
|
|
b0396b77bd | ||
|
|
07c2314376 | ||
|
|
cee140e49f | ||
|
|
a3963af6e7 | ||
|
|
8ff28418b3 | ||
|
|
08a5c2f2b3 | ||
|
|
286bd5b19e | ||
|
|
59f3c2e3ad | ||
|
|
ea1b394061 | ||
|
|
5dc1af76e8 | ||
|
|
771bbd834b | ||
|
|
418724f860 | ||
|
|
2ecf48bc60 | ||
|
|
d19b942d2c | ||
|
|
08e724759d | ||
|
|
80031bc80b | ||
|
|
beb53c672c | ||
|
|
0dc75bf737 | ||
|
|
59008fb964 | ||
|
|
b119e5ee00 | ||
|
|
01d0f4c72a | ||
|
|
9fe3f1a4db | ||
|
|
60bf112ebd | ||
|
|
45aa443889 | ||
|
|
08f4922860 | ||
|
|
899970405a | ||
|
|
b4a3960e43 | ||
|
|
0514e62d78 | ||
|
|
3621e58d4c | ||
|
|
506b24026f | ||
|
|
98b67f5d56 | ||
|
|
33c95aa0e8 | ||
|
|
7d8f86bb6c | ||
|
|
d6717cc58b | ||
|
|
673f8e3b2a | ||
|
|
cae87ba414 | ||
|
|
13362fd53e | ||
|
|
d9f2ba0665 | ||
|
|
64a0f90a9a | ||
|
|
88300910a2 | ||
|
|
7face43d54 | ||
|
|
6a9f35ce2a | ||
|
|
effe76f251 | ||
|
|
58d5539cb8 | ||
|
|
d956ee06d0 | ||
|
|
8ddf91d13b | ||
|
|
3f65cd4f6d | ||
|
|
3ffdbf9d2b | ||
|
|
a51754b0e3 | ||
|
|
e8ee21e567 | ||
|
|
420f070035 | ||
|
|
c78cbc04d3 | ||
|
|
dddf565fbe | ||
|
|
0516ca810d | ||
|
|
fac67696a9 | ||
|
|
c62d4bd3fd | ||
|
|
877e1d4992 | ||
|
|
35b5cddd95 | ||
|
|
a86fb128d9 | ||
|
|
983daae971 | ||
|
|
9b687e3286 | ||
|
|
abfd0ffe35 | ||
|
|
407afa1d8c | ||
|
|
9bafe97ef6 | ||
|
|
cb151e79d8 | ||
|
|
7e6230d7b0 | ||
|
|
7d95de389a | ||
|
|
2ce187bc98 | ||
|
|
100671265e | ||
|
|
965d5af631 | ||
|
|
a19f356a66 | ||
|
|
a520357a23 | ||
|
|
d92d130a7c | ||
|
|
c8dd7d2f04 | ||
|
|
b85b589675 | ||
|
|
9b4cbed28f | ||
|
|
6b1794d32f | ||
|
|
aefe4b6849 | ||
|
|
a68395174a | ||
|
|
8a56e789b7 | ||
|
|
06ef146c5b | ||
|
|
4121cbd400 | ||
|
|
2d3bb8798d | ||
|
|
b7b6ac0f87 | ||
|
|
e5fb65d75e | ||
|
|
290b73f3d9 | ||
|
|
f717e87306 | ||
|
|
b80ac7c60d | ||
|
|
997a4732ec | ||
|
|
6e08f11578 | ||
|
|
87c4814e6f | ||
|
|
2e0e009719 | ||
|
|
77399038e9 | ||
|
|
fae66619fb | ||
|
|
d979b9ec0c | ||
|
|
975319a65d | ||
|
|
7b5aa4ba2d | ||
|
|
670739c82b | ||
|
|
8511bd15a8 | ||
|
|
06d3c89274 | ||
|
|
e13f3eb2f1 | ||
|
|
001918d613 | ||
|
|
c859c3aa40 | ||
|
|
2bce019677 | ||
|
|
6ba3386157 | ||
|
|
51e66d98f9 | ||
|
|
6484804649 | ||
|
|
b2a05c099d | ||
|
|
898331da4c | ||
|
|
9044e782b2 | ||
|
|
aedb985941 | ||
|
|
9ba22e4058 | ||
|
|
ab0afb61ae | ||
|
|
bf00ba0adc | ||
|
|
d564cf1b62 | ||
|
|
544dfb3b24 | ||
|
|
21eeab6c3c | ||
|
|
cf9af910be | ||
|
|
e2e65f93af | ||
|
|
b271567428 | ||
|
|
a88a295d9a | ||
|
|
38ba31ca7d | ||
|
|
1c8ff2493b | ||
|
|
5c9f62e6b6 | ||
|
|
6fd8018c52 | ||
|
|
d900ddf5f1 | ||
|
|
e3a8d311ce | ||
|
|
8bbc3d5470 | ||
|
|
00cf987cec | ||
|
|
7e5dcae64a | ||
|
|
cb67237d7c | ||
|
|
4be848c440 | ||
|
|
b33422b066 | ||
|
|
77418684da | ||
|
|
08cf9f7774 | ||
|
|
9f2fabf9c0 | ||
|
|
e2e1c7be44 | ||
|
|
ba030ba197 | ||
|
|
a4f37bdc16 | ||
|
|
f6c7b00562 | ||
|
|
b951614f7c | ||
|
|
f1dfdb3494 | ||
|
|
ffbd25b13b | ||
|
|
4f03fe2420 | ||
|
|
ffdb386afa | ||
|
|
9eeeaa79d0 | ||
|
|
4163142d1e | ||
|
|
71cddc67e0 | ||
|
|
2cbc4eb54f | ||
|
|
fc94fb4221 | ||
|
|
85ed41c590 | ||
|
|
59f889a200 | ||
|
|
7dc476fe02 | ||
|
|
f5070f1ff1 | ||
|
|
15283e13f0 | ||
|
|
0cde5bf8e7 | ||
|
|
fe3a1a023d | ||
|
|
369dcff2bd | ||
|
|
ed341a12b1 | ||
|
|
00e523e268 | ||
|
|
4e25aedbe7 | ||
|
|
45bd3cbb7c | ||
|
|
8e34bc2130 | ||
|
|
457fc96332 | ||
|
|
e2186401bf | ||
|
|
a19d916ef0 | ||
|
|
42bc7dc644 | ||
|
|
f9d6308154 | ||
|
|
dcb6627fb1 | ||
|
|
1d5c71d617 | ||
|
|
d5157fb868 | ||
|
|
98096845dc | ||
|
|
28ac97f4c9 | ||
|
|
2fc39e3979 | ||
|
|
9c89c3c2b8 | ||
|
|
15c7747f48 | ||
|
|
940d4b7fd4 | ||
|
|
a1c8ce754e | ||
|
|
5e6364850a | ||
|
|
8df52e7595 | ||
|
|
1e004d6700 | ||
|
|
4570f9cd38 | ||
|
|
601cd39ba4 | ||
|
|
923475fab2 | ||
|
|
21ce5dfad4 | ||
|
|
2bd3592aad | ||
|
|
44f083ca23 | ||
|
|
cc7271f0a2 | ||
|
|
11a1a1134d | ||
|
|
70cfa13e67 | ||
|
|
291f90b2b2 | ||
|
|
d0ae7eb841 | ||
|
|
8444102cca | ||
|
|
5a1c052c77 | ||
|
|
fb7eeece54 | ||
|
|
d479bcece9 | ||
|
|
deefac2347 | ||
|
|
53808a04b7 | ||
|
|
a1785c2658 | ||
|
|
601cf46b1e | ||
|
|
6bba1b1cbd | ||
|
|
ab0122420b | ||
|
|
74ae2aab91 | ||
|
|
c5fa131a44 | ||
|
|
8dcf4dda9f | ||
|
|
cd59125ada | ||
|
|
d284eb6738 | ||
|
|
a78cc943cc | ||
|
|
7ddeda1595 | ||
|
|
f02dfa5e14 | ||
|
|
b2a4f20381 | ||
|
|
dec3c354f0 | ||
|
|
2ee581d48d | ||
|
|
d74c3a3fc5 | ||
|
|
405d9f0c09 | ||
|
|
db25c5babe | ||
|
|
d5c90ab8da | ||
|
|
81a5c143d6 | ||
|
|
18c6edd310 | ||
|
|
a1d7a0d9ca | ||
|
|
5d5078534d | ||
|
|
537a968e2e | ||
|
|
ebf2176618 | ||
|
|
f435f8eff5 | ||
|
|
4520d1d29f | ||
|
|
f8c16edaae | ||
|
|
ea86065d99 | ||
|
|
ed5c7b126c | ||
|
|
806e015823 | ||
|
|
41c4cbc96c | ||
|
|
143a0b2c41 | ||
|
|
f582db3f11 | ||
|
|
103855ca50 | ||
|
|
70c6fe4c68 | ||
|
|
c85d45050f | ||
|
|
16a39ac76a | ||
|
|
fdc7519db0 | ||
|
|
83af0c5ec7 | ||
|
|
ee22686bff | ||
|
|
7dc1f9736a | ||
|
|
6e2aa37d75 | ||
|
|
fbaa8a31cf | ||
|
|
fa89a5ad24 | ||
|
|
464deaf35e | ||
|
|
9cc8f50ff7 | ||
|
|
c17a1184cc | ||
|
|
b5004739c3 | ||
|
|
d01c0fa72b | ||
|
|
bb9f3d5aab | ||
|
|
b23daa6735 | ||
|
|
bb8b0125b6 | ||
|
|
8e5f44f998 | ||
|
|
9015af4902 | ||
|
|
7902a091a9 | ||
|
|
615357befb | ||
|
|
34b26c81dc | ||
|
|
ea99a54e1b | ||
|
|
f726826391 | ||
|
|
dc5148c856 | ||
|
|
373c11ffee | ||
|
|
bea1f019b4 | ||
|
|
29c98777e9 | ||
|
|
9eb5029128 | ||
|
|
33607796d1 | ||
|
|
f23fa0c335 | ||
|
|
e6cfecfc1a | ||
|
|
44346d5b33 | ||
|
|
19da68884b | ||
|
|
544349de8d | ||
|
|
6f90b1a0e3 | ||
|
|
fbbb9163d7 | ||
|
|
445581eefa | ||
|
|
096c036ea2 | ||
|
|
b2e5801426 | ||
|
|
41061854f1 | ||
|
|
d91e625973 | ||
|
|
ec5b59a00c | ||
|
|
172790b18f | ||
|
|
de0e6ee955 | ||
|
|
69d7ff2ced | ||
|
|
3c237eac91 | ||
|
|
694c195024 | ||
|
|
c1f0281030 | ||
|
|
fa218cbcbd | ||
|
|
a89700acc2 | ||
|
|
82fb24e275 | ||
|
|
eef67a9383 | ||
|
|
1180d9fa2c | ||
|
|
57734f1d3c | ||
|
|
dace750829 | ||
|
|
f359a7167a | ||
|
|
39c0759d1b | ||
|
|
fee90334fb | ||
|
|
80db6e50ab | ||
|
|
1fa6c2d842 | ||
|
|
8b39c4c22c | ||
|
|
4b6fd5b5a8 | ||
|
|
f2d1909b6d | ||
|
|
7917fb96dc | ||
|
|
ad5fae98f1 | ||
|
|
8412eb36fe | ||
|
|
822455383b | ||
|
|
2d4357e4c0 | ||
|
|
6336407f15 | ||
|
|
38e3492137 | ||
|
|
255e426e2d | ||
|
|
fed1cf2a0d | ||
|
|
a5cb8c3fdc | ||
|
|
514d86144f | ||
|
|
21db7934c9 | ||
|
|
14e4b5c0e3 | ||
|
|
6478ad0411 | ||
|
|
81b7d432e9 | ||
|
|
dcbf5094f9 | ||
|
|
69ba181dfa | ||
|
|
88654275c1 | ||
|
|
62f28271ed | ||
|
|
0538130e78 | ||
|
|
95a3a16227 | ||
|
|
bfbf89ff69 | ||
|
|
e630589489 | ||
|
|
99d1e51f36 | ||
|
|
ab42ccd889 | ||
|
|
767af25aa0 | ||
|
|
7575dd82ce | ||
|
|
9307bbd09e | ||
|
|
68a9ec09e6 | ||
|
|
f8a548401f | ||
|
|
26d1f8e58c | ||
|
|
8772e25c8e | ||
|
|
378ac82c8c | ||
|
|
fcb1292ffd | ||
|
|
18112ee40f | ||
|
|
fa8b9dfe99 | ||
|
|
e7dff4edc9 | ||
|
|
7614d12363 | ||
|
|
3189317b2d | ||
|
|
410d11248f | ||
|
|
28e1f5ac8a | ||
|
|
bafe73d296 | ||
|
|
f17ce50f17 | ||
|
|
18f5872be1 | ||
|
|
fdccaaa65e | ||
|
|
6a55172924 | ||
|
|
1e41af77fa | ||
|
|
e169a6e02d | ||
|
|
ef55c834af | ||
|
|
3812ad92ac | ||
|
|
0bdb402e7b | ||
|
|
1588ea250b | ||
|
|
47f603877b | ||
|
|
9adc803206 | ||
|
|
031e290017 | ||
|
|
a9ab2d0110 | ||
|
|
9592b6087b | ||
|
|
5c34213b5d | ||
|
|
4435fe1e0a |
60
.github/workflows/test.yml
vendored
Normal file
60
.github/workflows/test.yml
vendored
Normal 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
13
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
56
common/test/rtsp-proxy.ts
Normal 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();
|
||||
@@ -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
|
||||
@@ -31,40 +33,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 +93,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
|
||||
################################################################
|
||||
|
||||
@@ -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
22
docker/Dockerfile.nvidia
Normal 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
|
||||
@@ -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
3
docker/docker-build-nvidia.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
./docker-build.sh
|
||||
|
||||
docker build -t koush/scrypted:18-bullseye-full.nvidia -f Dockerfile.nvidia
|
||||
@@ -38,6 +38,8 @@ services:
|
||||
# - /dev/ttyACM0:/dev/ttyACM0
|
||||
# all usb devices, such as coral tpu
|
||||
# - /dev/bus/usb:/dev/bus/usb
|
||||
# intel hardware accelerated video decoding
|
||||
# - /dev/dri:/dev/dri
|
||||
|
||||
volumes:
|
||||
- ~/.scrypted/volume:/server/volume
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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..."
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
################################################################
|
||||
|
||||
@@ -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
|
||||
@@ -26,40 +28,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
1
external/HAP-NodeJS
vendored
Submodule external/HAP-NodeJS deleted from 3fe1f920f5
1
external/push-receiver
vendored
1
external/push-receiver
vendored
Submodule external/push-receiver deleted from d054e083d6
2
external/ring-client-api
vendored
2
external/ring-client-api
vendored
Submodule external/ring-client-api updated: 7ef505251f...81f6570f59
2
external/werift
vendored
2
external/werift
vendored
Submodule external/werift updated: 3d58683cdf...140faa891d
@@ -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
|
||||
|
||||
8
packages/cli/.vscode/launch.json
vendored
8
packages/cli/.vscode/launch.json
vendored
@@ -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": [
|
||||
|
||||
717
packages/cli/package-lock.json
generated
717
packages/cli/package-lock.json
generated
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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/**/*"
|
||||
],
|
||||
}
|
||||
359
packages/client/package-lock.json
generated
359
packages/client/package-lock.json
generated
@@ -1,77 +1,35 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"lockfileVersion": 2,
|
||||
"version": "1.1.51",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"version": "1.1.51",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"adm-zip": "^0.5.9",
|
||||
"@scrypted/types": "^0.2.80",
|
||||
"axios": "^0.25.0",
|
||||
"engine.io-client": "^6.2.2",
|
||||
"linkfs": "^2.1.0",
|
||||
"memfs": "^3.4.1",
|
||||
"engine.io-client": "^6.4.0",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
},
|
||||
"../../common": {
|
||||
"name": "@scrypted/common",
|
||||
"version": "1.0.1",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": "file:../sdk",
|
||||
"@scrypted/server": "file:../server",
|
||||
"http-auth-utils": "^3.0.2",
|
||||
"node-fetch-commonjs": "^3.1.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../sdk/types": {
|
||||
"name": "@scrypted/types",
|
||||
"version": "0.0.9",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../common": {
|
||||
"extraneous": true
|
||||
},
|
||||
"../sdk/types": {
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@scrypted/types": {
|
||||
"version": "0.2.64",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
|
||||
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
|
||||
"version": "0.2.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",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/@types/adm-zip": {
|
||||
"version": "0.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
|
||||
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ip": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
|
||||
@@ -82,19 +40,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
|
||||
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
|
||||
"version": "18.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.2.tgz",
|
||||
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
@@ -120,12 +70,12 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
@@ -139,29 +89,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
|
||||
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"ws": "~8.2.3",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
|
||||
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
|
||||
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -177,25 +127,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
@@ -209,7 +154,7 @@
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -220,22 +165,6 @@
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/linkfs": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
|
||||
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
|
||||
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
|
||||
"dependencies": {
|
||||
"fs-monkey": "1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -255,7 +184,7 @@
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -263,7 +192,7 @@
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -283,9 +212,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -298,12 +227,12 @@
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -328,209 +257,5 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": {
|
||||
"version": "0.2.64",
|
||||
"resolved": "https://registry.npmjs.org/@scrypted/types/-/types-0.2.64.tgz",
|
||||
"integrity": "sha512-8x+EVlsJ5MGJ5HxPcVxV5p5RakP9zivqhTkzgEUUbfGDUXUmv1BYlNy/AESkSNKR26idEiZrKD1VfE67hPIH8A=="
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"@types/adm-zip": {
|
||||
"version": "0.4.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz",
|
||||
"integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/ip": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ip/-/ip-1.1.0.tgz",
|
||||
"integrity": "sha512-dwNe8gOoF70VdL6WJBwVHtQmAX4RMd62M+mAB9HQFjG1/qiCLM/meRy95Pd14FYBbEDwCq7jgJs89cHpLBu4HQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
|
||||
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==",
|
||||
"dev": true
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
|
||||
"integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.7"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
|
||||
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.0.3",
|
||||
"ws": "~8.2.3",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
|
||||
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.14.8",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
|
||||
},
|
||||
"fs-monkey": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz",
|
||||
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"linkfs": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/linkfs/-/linkfs-2.1.0.tgz",
|
||||
"integrity": "sha512-kmsGcmpvjStZ0ATjuHycBujtNnXiZR28BTivEu0gAMDTT7GEyodcK6zSRtu6xsrdorrPZEIN380x7BD7xEYkew=="
|
||||
},
|
||||
"memfs": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz",
|
||||
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==",
|
||||
"requires": {
|
||||
"fs-monkey": "1.0.3"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"requires": {}
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/client",
|
||||
"version": "1.1.37",
|
||||
"version": "1.1.51",
|
||||
"description": "",
|
||||
"main": "dist/packages/client/src/index.js",
|
||||
"scripts": {
|
||||
@@ -12,18 +12,14 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.34",
|
||||
"@types/ip": "^1.1.0",
|
||||
"@types/node": "^17.0.17",
|
||||
"typescript": "^4.7.4"
|
||||
"@types/node": "^18.14.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/types": "^0.2.64",
|
||||
"adm-zip": "^0.5.9",
|
||||
"@scrypted/types": "^0.2.80",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
4
packages/rpc/package-lock.json
generated
4
packages/rpc/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/rpc",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
|
||||
2
plugins/alexa/.vscode/settings.json
vendored
2
plugins/alexa/.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
"scrypted.debugHost": "10.10.0.50",
|
||||
}
|
||||
325
plugins/alexa/package-lock.json
generated
325
plugins/alexa/package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/alexa",
|
||||
"version": "0.0.20",
|
||||
"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": "^0.24.0",
|
||||
"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
221
plugins/alexa/src/alexa.ts
Normal 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
131
plugins/alexa/src/common.ts
Normal 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);
|
||||
}
|
||||
34
plugins/alexa/src/handlers.ts
Normal file
34
plugins/alexa/src/handlers.ts
Normal 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);
|
||||
});
|
||||
@@ -1,109 +1,268 @@
|
||||
import axios from 'axios';
|
||||
import sdk, { HttpRequest, HttpRequestHandler, HttpResponse, MixinProvider, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface } 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 {
|
||||
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,
|
||||
json: true,
|
||||
json: true
|
||||
},
|
||||
syncedDevices: {
|
||||
multiple: true,
|
||||
hide: 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;
|
||||
|
||||
if (!this.storageSettings.values.syncedDevices.includes(eventSource.id))
|
||||
return;
|
||||
|
||||
const supportedType = supportedTypes.get(eventSource.type);
|
||||
if (!supportedType) {
|
||||
this.console.warn(`${eventSource.name} no longer supported type?`);
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
"context": report?.context
|
||||
}
|
||||
|
||||
data = addOnline(data, eventSource);
|
||||
data = addBattery(data, eventSource);
|
||||
data = addPowerSensor(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,
|
||||
};
|
||||
|
||||
await this.postEvent(data);
|
||||
});
|
||||
this.start();
|
||||
}
|
||||
|
||||
async getMixin(mixinDevice: any, mixinDeviceInterfaces: ScryptedInterface[], mixinDeviceState: { [key: string]: any; }): Promise<any> {
|
||||
return mixinDevice;
|
||||
async start() {
|
||||
|
||||
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));
|
||||
|
||||
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);
|
||||
if (device.mixins?.includes(this.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;
|
||||
}
|
||||
this.console.log('release mixin', id);
|
||||
this.log.a(`${device.name} was removed. The Alexa plugin will reload momentarily.`);
|
||||
deviceManager.requestRestart();
|
||||
|
||||
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"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": eventSource.id,
|
||||
},
|
||||
payload: report?.event?.payload
|
||||
},
|
||||
context: report?.context
|
||||
} as ChangeReport;
|
||||
|
||||
data = addOnline(data, eventSource);
|
||||
data = addBattery(data, eventSource);
|
||||
|
||||
// nothing to report
|
||||
if (data.context === undefined && data.event.payload === undefined)
|
||||
return;
|
||||
|
||||
data = await this.addAccessToken(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);
|
||||
}
|
||||
|
||||
readonly endpoints: string[] = [
|
||||
'api.amazonalexa.com',
|
||||
'api.eu.amazonalexa.com',
|
||||
'api.fe.amazonalexa.com'
|
||||
];
|
||||
|
||||
async getAlexaEndpoint() : Promise<string> {
|
||||
if (this.storageSettings.values.apiEndpoint)
|
||||
return this.storageSettings.values.apiEndpoint;
|
||||
|
||||
try {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const response = await axios.get(`https://${this.endpoints[0]}/v1/alexaApiEndpoint`, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
}
|
||||
});
|
||||
|
||||
const endpoint: string = response.data.endpoints[0];
|
||||
this.storageSettings.values.apiEndpoint = endpoint;
|
||||
return endpoint;
|
||||
} catch (err) {
|
||||
this.console.error(err);
|
||||
|
||||
// default to NA/RoW endpoint if we can't get the endpoint.
|
||||
return this.endpoints[0];
|
||||
}
|
||||
}
|
||||
|
||||
async postEvent(data: any) {
|
||||
const accessToken = await this.getAccessToken();
|
||||
const endpoint = await this.getAlexaEndpoint();
|
||||
const self = this;
|
||||
|
||||
return axios.post('https://api.amazonalexa.com/v3/events', data, {
|
||||
debug("send event to alexa", data);
|
||||
|
||||
return axios.post(`https://${endpoint}/v3/events`, data, {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + accessToken,
|
||||
}
|
||||
@@ -117,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,
|
||||
@@ -145,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;
|
||||
|
||||
@@ -176,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;
|
||||
@@ -263,10 +468,10 @@ 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;
|
||||
|
||||
const self = this;
|
||||
@@ -274,29 +479,17 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
self.console.error(`Failed to handle the AcceptGrant directive because ${reason}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
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",
|
||||
@@ -306,25 +499,27 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
},
|
||||
"payload": {}
|
||||
}
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
this.console.error(`AcceptGrant.Response failed because ${error}`);
|
||||
|
||||
this.storageSettings.values.tokenInfo = undefined;
|
||||
this.storageSettings.values.apiEndpoint = undefined;
|
||||
this.accessToken = undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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`,
|
||||
@@ -334,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"
|
||||
// },
|
||||
@@ -354,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",
|
||||
@@ -372,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,
|
||||
@@ -461,36 +614,75 @@ class AlexaPlugin extends AutoenableMixinProvider implements HttpRequestHandler,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {v4 as uuidv4} from 'uuid';
|
||||
|
||||
export function createMessageId() {
|
||||
return uuidv4();
|
||||
}
|
||||
@@ -1,189 +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: {
|
||||
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));
|
||||
});
|
||||
|
||||
194
plugins/alexa/src/types/camera/capabilities.ts
Normal file
194
plugins/alexa/src/types/camera/capabilities.ts
Normal 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;
|
||||
};
|
||||
159
plugins/alexa/src/types/camera/handlers.ts
Normal file
159
plugins/alexa/src/types/camera/handlers.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
31
plugins/alexa/src/types/garagedoor/handlers.ts
Normal file
31
plugins/alexa/src/types/garagedoor/handlers.ts
Normal 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);
|
||||
@@ -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';
|
||||
179
plugins/alexa/src/types/securitysystem.ts
Normal file
179
plugins/alexa/src/types/securitysystem.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
196
plugins/alexa/src/types/sensor.ts
Normal file
196
plugins/alexa/src/types/sensor.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
71
plugins/alexa/src/types/switch.ts
Normal file
71
plugins/alexa/src/types/switch.ts
Normal 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>;
|
||||
}
|
||||
});
|
||||
55
plugins/alexa/src/types/switch/handlers.ts
Normal file
55
plugins/alexa/src/types/switch/handlers.ts
Normal 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);
|
||||
});
|
||||
20
plugins/amcrest/package-lock.json
generated
20
plugins/amcrest/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
42
plugins/amcrest/src/probe.ts
Normal file
42
plugins/amcrest/src/probe.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
2
plugins/arlo/.vscode/settings.json
vendored
2
plugins/arlo/.vscode/settings.json
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
6
plugins/arlo/package-lock.json
generated
6
plugins/arlo/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/arlo",
|
||||
"version": "0.6.5",
|
||||
"version": "0.7.13",
|
||||
"description": "Arlo Plugin for Scrypted",
|
||||
"keywords": [
|
||||
"scrypted",
|
||||
|
||||
@@ -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')
|
||||
@@ -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):
|
||||
|
||||
69
plugins/arlo/src/arlo_plugin/base.py
Normal file
69
plugins/arlo/src/arlo_plugin/base.py
Normal 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 []
|
||||
71
plugins/arlo/src/arlo_plugin/basestation.py
Normal file
71
plugins/arlo/src/arlo_plugin/basestation.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
72
plugins/arlo/src/arlo_plugin/siren.py
Normal file
72
plugins/arlo/src/arlo_plugin/siren.py
Normal 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,
|
||||
}
|
||||
54
plugins/arlo/src/arlo_plugin/spotlight.py
Normal file
54
plugins/arlo/src/arlo_plugin/spotlight.py
Normal 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
|
||||
@@ -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
|
||||
156
plugins/arlo/src/arlo_plugin/vss.py
Normal file
156
plugins/arlo/src/arlo_plugin/vss.py
Normal 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()
|
||||
@@ -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/
|
||||
|
||||
4680
plugins/bticino/package-lock.json
generated
4680
plugins/bticino/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
407
plugins/bticino/src/bticino-camera.ts
Normal file
407
plugins/bticino/src/bticino-camera.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
32
plugins/bticino/src/bticino-inviteHandler.ts
Normal file
32
plugins/bticino/src/bticino-inviteHandler.ts
Normal 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 )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
56
plugins/bticino/src/bticino-lock.ts
Normal file
56
plugins/bticino/src/bticino-lock.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
69
plugins/bticino/src/bticino-voicemailHandler.ts
Normal file
69
plugins/bticino/src/bticino-voicemailHandler.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
71
plugins/bticino/src/persistent-sip-manager.ts
Normal file
71
plugins/bticino/src/persistent-sip-manager.ts
Normal 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 )
|
||||
}
|
||||
}
|
||||
59
plugins/bticino/src/sip-helper.ts
Normal file
59
plugins/bticino/src/sip-helper.ts
Normal 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
|
||||
}
|
||||
}
|
||||
78
plugins/bticino/src/storage-settings.ts
Normal file
78
plugins/bticino/src/storage-settings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
307
plugins/cloud/package-lock.json
generated
307
plugins/cloud/package-lock.json
generated
@@ -1,26 +1,28 @@
|
||||
{
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.1.11",
|
||||
"version": "0.1.13",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/cloud",
|
||||
"version": "0.1.11",
|
||||
"version": "0.1.13",
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "../../external/push-receiver",
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"axios": "^0.25.0",
|
||||
"bpmux": "^8.1.3",
|
||||
"debug": "^4.3.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nat-upnp": "file:./node-nat-upnp",
|
||||
"query-string": "^6.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/http-proxy": "^1.17.5",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^18.11.18"
|
||||
}
|
||||
@@ -40,39 +42,9 @@
|
||||
"@types/node": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"../../external/push-receiver": {
|
||||
"name": "@eneris/push-receiver",
|
||||
"version": "3.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.1",
|
||||
"http_ece": "^1.0.5",
|
||||
"long": "^5.2.0",
|
||||
"protobufjs": "^6.11.2",
|
||||
"request-promise": "^4.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^28.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^17.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-jest": "^26.4.6",
|
||||
"http-proxy": "^1.16.2",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^28.0.2",
|
||||
"ts-jest": "^28.0.4",
|
||||
"typescript": "^4.4.3",
|
||||
"yargs": "^17.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"name": "@scrypted/sdk",
|
||||
"version": "0.2.56",
|
||||
"version": "0.2.82",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
@@ -108,8 +80,82 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eneris/push-receiver": {
|
||||
"resolved": "../../external/push-receiver",
|
||||
"link": true
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
|
||||
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
|
||||
"dependencies": {
|
||||
"axios": "^1.2.1",
|
||||
"http_ece": "^1.0.5",
|
||||
"long": "^5.2.1",
|
||||
"protobufjs": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@eneris/push-receiver/node_modules/axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"node_modules/@scrypted/common": {
|
||||
"resolved": "../../common",
|
||||
@@ -137,6 +183,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
@@ -155,8 +207,7 @@
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "1.1.1",
|
||||
@@ -730,6 +781,17 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/http_ece": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
|
||||
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
|
||||
"dependencies": {
|
||||
"urlsafe-base64": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
@@ -1160,6 +1222,11 @@
|
||||
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
|
||||
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -1432,6 +1499,29 @@
|
||||
"node": ">= 0.6.6"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
|
||||
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -1636,6 +1726,11 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/urlsafe-base64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
|
||||
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
|
||||
},
|
||||
"node_modules/utile": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
|
||||
@@ -1889,28 +1984,82 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": {
|
||||
"version": "file:../../external/push-receiver",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@eneris/push-receiver/-/push-receiver-3.1.4.tgz",
|
||||
"integrity": "sha512-KgSydrAmPwcc/xpvRmkvImUMts8uDl+4sUaGypPmD/kn3jhGuDVjzqhnxbSbdycm61rHZRM8NhUZrYUTEZgYlg==",
|
||||
"requires": {
|
||||
"@types/jest": "^28.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": "^17.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"axios": "^0.27.1",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-plugin-jest": "^26.4.6",
|
||||
"axios": "^1.2.1",
|
||||
"http_ece": "^1.0.5",
|
||||
"http-proxy": "^1.16.2",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^28.0.2",
|
||||
"long": "^5.2.0",
|
||||
"protobufjs": "^6.11.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"ts-jest": "^28.0.4",
|
||||
"typescript": "^4.4.3",
|
||||
"yargs": "^17.2.1"
|
||||
"long": "^5.2.1",
|
||||
"protobufjs": "^7.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
|
||||
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
|
||||
},
|
||||
"@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
|
||||
},
|
||||
"@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
|
||||
},
|
||||
"@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
|
||||
},
|
||||
"@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
|
||||
},
|
||||
"@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
|
||||
},
|
||||
"@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
|
||||
},
|
||||
"@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
|
||||
},
|
||||
"@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"@scrypted/common": {
|
||||
"version": "file:../../common",
|
||||
"requires": {
|
||||
@@ -1964,6 +2113,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.191",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
|
||||
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/ms": {
|
||||
"version": "0.7.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
|
||||
@@ -1982,8 +2137,7 @@
|
||||
"@types/node": {
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "1.1.1",
|
||||
@@ -2399,6 +2553,14 @@
|
||||
"integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==",
|
||||
"dev": true
|
||||
},
|
||||
"http_ece": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.1.0.tgz",
|
||||
"integrity": "sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==",
|
||||
"requires": {
|
||||
"urlsafe-base64": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
@@ -2737,6 +2899,11 @@
|
||||
"integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==",
|
||||
"dev": true
|
||||
},
|
||||
"long": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
|
||||
"integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
@@ -2966,6 +3133,25 @@
|
||||
"winston": "0.8.x"
|
||||
}
|
||||
},
|
||||
"protobufjs": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.2.tgz",
|
||||
"integrity": "sha512-++PrQIjrom+bFDPpfmqXfAGSQs40116JRrqqyf53dymUMvvb5d/LMRyicRoF1AUKoXVS1/IgJXlEgcpr4gTF3Q==",
|
||||
"requires": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -3109,6 +3295,11 @@
|
||||
"integrity": "sha512-OHbMkscHFRcNWEcW80fYhCrzAjheSIBwJChpFaBqA6zEz53nxumqi6ukciRb/UA0/v2nDNMk28ce/uBbYRDsng==",
|
||||
"dev": true
|
||||
},
|
||||
"urlsafe-base64": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz",
|
||||
"integrity": "sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA=="
|
||||
},
|
||||
"utile": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/utile/-/utile-0.2.1.tgz",
|
||||
|
||||
@@ -37,21 +37,23 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@eneris/push-receiver": "^3.1.4",
|
||||
"@scrypted/common": "file:../../common",
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@eneris/push-receiver": "../../external/push-receiver",
|
||||
"axios": "^0.25.0",
|
||||
"bpmux": "^8.1.3",
|
||||
"debug": "^4.3.1",
|
||||
"http-proxy": "^1.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nat-upnp": "file:./node-nat-upnp",
|
||||
"query-string": "^6.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/http-proxy": "^1.17.5",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/nat-upnp": "^1.1.2",
|
||||
"@types/node": "^18.11.18"
|
||||
},
|
||||
"version": "0.1.11"
|
||||
"version": "0.1.13"
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDevice, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
|
||||
import sdk, { BufferConverter, DeviceProvider, HttpRequest, HttpRequestHandler, HttpResponse, OauthClient, PushHandler, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings } from "@scrypted/sdk";
|
||||
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
||||
import axios from 'axios';
|
||||
import bpmux from 'bpmux';
|
||||
import crypto from 'crypto';
|
||||
import { once } from 'events';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import HttpProxy from 'http-proxy';
|
||||
import https from 'https';
|
||||
import throttle from "lodash/throttle";
|
||||
import upnp from 'nat-upnp';
|
||||
import net from 'net';
|
||||
import net, { AddressInfo } from 'net';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import qs from 'query-string';
|
||||
import { Duplex } from 'stream';
|
||||
import tls from 'tls';
|
||||
import Url from 'url';
|
||||
import type { CORSControlLegacy } from '../../../server/src/services/cors';
|
||||
import { createSelfSignedCertificate } from '../../../server/src/cert';
|
||||
import { PushManager } from './push';
|
||||
import tls from 'tls';
|
||||
|
||||
const { deviceManager, endpointManager, systemManager } = sdk;
|
||||
|
||||
@@ -547,6 +547,8 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
};
|
||||
|
||||
const handler = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.console.log(req.socket?.remoteAddress, req.url);
|
||||
|
||||
const url = Url.parse(req.url);
|
||||
if (url.path.startsWith('/web/oauth/callback') && url.query) {
|
||||
const query = qs.parse(url.query);
|
||||
@@ -620,7 +622,7 @@ class ScryptedCloud extends ScryptedDeviceBase implements OauthClient, Settings,
|
||||
});
|
||||
this.proxy.on('error', () => { });
|
||||
this.proxy.on('proxyRes', (res, req) => {
|
||||
res.headers['X-Scrypted-Cloud'] = 'true';
|
||||
res.headers['X-Scrypted-Cloud'] = req.headers['x-scrypted-cloud'];
|
||||
res.headers['X-Scrypted-Direct-Address'] = req.headers['x-scrypted-direct-address'];
|
||||
res.headers['Access-Control-Expose-Headers'] = 'X-Scrypted-Cloud, X-Scrypted-Direct-Address';
|
||||
});
|
||||
|
||||
4
plugins/core/package-lock.json
generated
4
plugins/core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.91",
|
||||
"version": "0.1.110",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.91",
|
||||
"version": "0.1.110",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scrypted/common": "file:../../common",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/core",
|
||||
"version": "0.1.91",
|
||||
"version": "0.1.110",
|
||||
"description": "Scrypted Core plugin. Provides the UI, websocket, and engine.io APIs.",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -91,6 +92,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(
|
||||
{
|
||||
@@ -237,3 +249,9 @@ class ScryptedCore extends ScryptedDeviceBase implements HttpRequestHandler, Eng
|
||||
}
|
||||
|
||||
export default ScryptedCore;
|
||||
|
||||
export async function fork() {
|
||||
return {
|
||||
tsCompile,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
118
plugins/core/ui/package-lock.json
generated
118
plugins/core/ui/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
color="primary" icon="mdi-vuetify" border="left">
|
||||
<template v-slot:prepend>
|
||||
<v-icon class="white--text mr-3" size="sm" color="#a9afbb">{{
|
||||
getAlertIcon(alert)
|
||||
getAlertIcon(alert)
|
||||
}}</v-icon>
|
||||
</template>
|
||||
<div class="caption">{{ alert.title }}</div>
|
||||
@@ -185,7 +185,8 @@
|
||||
<LogCard :rows="15" :logRoute="`/device/${id}/`"></LogCard>
|
||||
</v-flex>
|
||||
|
||||
<v-flex xs12 v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
|
||||
<v-flex xs12
|
||||
v-if="!device.interfaces.includes(ScryptedInterface.Settings) && (availableMixins.length || deviceIsEditable(device))">
|
||||
<Settings :device="device"></Settings>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
@@ -214,6 +215,7 @@ import Notifier from "../interfaces/Notifier.vue";
|
||||
import OnOff from "../interfaces/OnOff.vue";
|
||||
import Brightness from "../interfaces/Brightness.vue";
|
||||
import Battery from "../interfaces/Battery.vue";
|
||||
import Charger from "../interfaces/Charger.vue";
|
||||
import Lock from "../interfaces/Lock.vue";
|
||||
import ColorSettingHsv from "../interfaces/ColorSettingHsv.vue";
|
||||
import ColorSettingRgb from "../interfaces/ColorSettingRgb.vue";
|
||||
@@ -240,6 +242,7 @@ import Scene from "../interfaces/Scene.vue";
|
||||
import TemperatureSetting from "../interfaces/TemperatureSetting.vue";
|
||||
import PositionSensor from "../interfaces/sensors/PositionSensor.vue";
|
||||
import DeviceProvider from "../interfaces/DeviceProvider.vue";
|
||||
import ObjectDetection from "../interfaces/ObjectDetection.vue";
|
||||
import MixinProvider from "../interfaces/MixinProvider.vue";
|
||||
import Readme from "../interfaces/Readme.vue";
|
||||
import Scriptable from "../interfaces/automation/Scriptable.vue";
|
||||
@@ -261,6 +264,7 @@ const cardHeaderInterfaces = [
|
||||
ScryptedInterface.AudioSensor,
|
||||
ScryptedInterface.HumiditySensor,
|
||||
ScryptedInterface.Thermometer,
|
||||
ScryptedInterface.Charger,
|
||||
ScryptedInterface.Battery,
|
||||
ScryptedInterface.Lock,
|
||||
ScryptedInterface.OnOff,
|
||||
@@ -286,7 +290,9 @@ const leftInterfaces = [
|
||||
ScryptedInterface.DeviceProvider,
|
||||
ScryptedInterface.Readme,
|
||||
];
|
||||
const leftAboveInterfaces = [ScryptedInterface.Camera];
|
||||
const leftAboveInterfaces = [
|
||||
ScryptedInterface.Camera,
|
||||
];
|
||||
|
||||
const noCardInterfaces = [
|
||||
ScryptedInterface.Camera,
|
||||
@@ -294,7 +300,10 @@ const noCardInterfaces = [
|
||||
ScryptedInterface.Scriptable,
|
||||
];
|
||||
|
||||
const aboveInterfaces = [ScryptedInterface.Scriptable];
|
||||
const aboveInterfaces = [
|
||||
ScryptedInterface.ObjectDetection,
|
||||
ScryptedInterface.Scriptable
|
||||
];
|
||||
|
||||
const cardActionInterfaces = [
|
||||
ScryptedInterface.OauthClient,
|
||||
@@ -355,6 +364,7 @@ export default {
|
||||
|
||||
Lock,
|
||||
OnOff,
|
||||
Charger,
|
||||
Battery,
|
||||
Thermometer,
|
||||
HumiditySensor,
|
||||
@@ -379,6 +389,8 @@ export default {
|
||||
Automation,
|
||||
Program,
|
||||
Scriptable,
|
||||
|
||||
ObjectDetection,
|
||||
},
|
||||
mixins: [Mixin],
|
||||
data() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user