tecnoctl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tecnoctl-0.1.0/LICENSE +21 -0
- tecnoctl-0.1.0/PKG-INFO +79 -0
- tecnoctl-0.1.0/README.md +65 -0
- tecnoctl-0.1.0/pyproject.toml +19 -0
- tecnoctl-0.1.0/setup.cfg +4 -0
- tecnoctl-0.1.0/tecnoctl/__init__.py +5 -0
- tecnoctl-0.1.0/tecnoctl/cli.py +278 -0
- tecnoctl-0.1.0/tecnoctl/client.py +846 -0
- tecnoctl-0.1.0/tecnoctl.egg-info/PKG-INFO +79 -0
- tecnoctl-0.1.0/tecnoctl.egg-info/SOURCES.txt +13 -0
- tecnoctl-0.1.0/tecnoctl.egg-info/dependency_links.txt +1 -0
- tecnoctl-0.1.0/tecnoctl.egg-info/entry_points.txt +2 -0
- tecnoctl-0.1.0/tecnoctl.egg-info/requires.txt +2 -0
- tecnoctl-0.1.0/tecnoctl.egg-info/top_level.txt +1 -0
- tecnoctl-0.1.0/tests/test_protocol.py +197 -0
tecnoctl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enrico
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tecnoctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tecnoctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial client for the myTecnoalarm direct TCP protocol
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/EnricoDev1/tecnoctl
|
|
7
|
+
Project-URL: Issues, https://github.com/EnricoDev1/tecnoctl/issues
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: cryptography>=47
|
|
12
|
+
Requires-Dist: python-dotenv>=1
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# tecnoctl
|
|
16
|
+
|
|
17
|
+
Unofficial Python client and CLI for the direct TCP interface used by the
|
|
18
|
+
myTecnoalarm Android application. It does not use Tecnoalarm cloud, HTTP, or
|
|
19
|
+
video services.
|
|
20
|
+
|
|
21
|
+
> Experimental: This project was developed with AI assistance. The protocol was reverse-engineered and has not been tested on
|
|
22
|
+
> every hardware. Start with read-only commands. The protocol uses unauthenticated
|
|
23
|
+
> AES-CFB; never expose TCP port 10001 to the Internet. Use a trusted LAN or VPN.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python3 -m pip install .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
cp .env.example .env
|
|
35
|
+
# Edit .env with your credentials.
|
|
36
|
+
|
|
37
|
+
tecnoctl HOST status
|
|
38
|
+
tecnoctl HOST --verbose status
|
|
39
|
+
tecnoctl HOST --debug status
|
|
40
|
+
tecnoctl HOST zones
|
|
41
|
+
tecnoctl HOST watch
|
|
42
|
+
tecnoctl HOST arm 1
|
|
43
|
+
tecnoctl HOST disarm 1
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The CLI loads `.env` automatically; exported variables take precedence. Run
|
|
47
|
+
`tecnoctl --help` for all commands. CLI IDs are one-based. `watch` connects,
|
|
48
|
+
checks, and disconnects every 30 seconds, then prints JSON when an alarm starts,
|
|
49
|
+
a program starts arming or becomes armed/disarmed, or connectivity changes. The
|
|
50
|
+
minimum `--interval` is 5 seconds. Add `--debug` before any command for
|
|
51
|
+
connection and protocol diagnostics, or `--verbose` for quieter connection
|
|
52
|
+
status.
|
|
53
|
+
|
|
54
|
+
> `watch` is experimental and is not a primary alarm notification system. Some
|
|
55
|
+
> panels accept only one direct TCP client, so each check may briefly delay the
|
|
56
|
+
> official app.
|
|
57
|
+
|
|
58
|
+
## Python API
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from tecnoctl import AlarmClient
|
|
62
|
+
|
|
63
|
+
with AlarmClient("192.168.1.20", "network passphrase", "123456") as alarm:
|
|
64
|
+
print(alarm.status())
|
|
65
|
+
alarm.arm(0) # Library indexes are zero-based.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The class also exposes panel, program, remote, zone, permission, and event-log
|
|
69
|
+
queries. `alarm.watch()` yields JSON-friendly alarm and connection events. See
|
|
70
|
+
`AlarmClient` in `tecnoctl/client.py`.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
python3 -m unittest discover -s tests
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
MIT licensed. Tecnoalarm is a trademark of its owner; this project is not
|
|
79
|
+
affiliated with or endorsed by Tecnoalarm.
|
tecnoctl-0.1.0/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# tecnoctl
|
|
2
|
+
|
|
3
|
+
Unofficial Python client and CLI for the direct TCP interface used by the
|
|
4
|
+
myTecnoalarm Android application. It does not use Tecnoalarm cloud, HTTP, or
|
|
5
|
+
video services.
|
|
6
|
+
|
|
7
|
+
> Experimental: This project was developed with AI assistance. The protocol was reverse-engineered and has not been tested on
|
|
8
|
+
> every hardware. Start with read-only commands. The protocol uses unauthenticated
|
|
9
|
+
> AES-CFB; never expose TCP port 10001 to the Internet. Use a trusted LAN or VPN.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
python3 -m pip install .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## CLI
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
cp .env.example .env
|
|
21
|
+
# Edit .env with your credentials.
|
|
22
|
+
|
|
23
|
+
tecnoctl HOST status
|
|
24
|
+
tecnoctl HOST --verbose status
|
|
25
|
+
tecnoctl HOST --debug status
|
|
26
|
+
tecnoctl HOST zones
|
|
27
|
+
tecnoctl HOST watch
|
|
28
|
+
tecnoctl HOST arm 1
|
|
29
|
+
tecnoctl HOST disarm 1
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The CLI loads `.env` automatically; exported variables take precedence. Run
|
|
33
|
+
`tecnoctl --help` for all commands. CLI IDs are one-based. `watch` connects,
|
|
34
|
+
checks, and disconnects every 30 seconds, then prints JSON when an alarm starts,
|
|
35
|
+
a program starts arming or becomes armed/disarmed, or connectivity changes. The
|
|
36
|
+
minimum `--interval` is 5 seconds. Add `--debug` before any command for
|
|
37
|
+
connection and protocol diagnostics, or `--verbose` for quieter connection
|
|
38
|
+
status.
|
|
39
|
+
|
|
40
|
+
> `watch` is experimental and is not a primary alarm notification system. Some
|
|
41
|
+
> panels accept only one direct TCP client, so each check may briefly delay the
|
|
42
|
+
> official app.
|
|
43
|
+
|
|
44
|
+
## Python API
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from tecnoctl import AlarmClient
|
|
48
|
+
|
|
49
|
+
with AlarmClient("192.168.1.20", "network passphrase", "123456") as alarm:
|
|
50
|
+
print(alarm.status())
|
|
51
|
+
alarm.arm(0) # Library indexes are zero-based.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The class also exposes panel, program, remote, zone, permission, and event-log
|
|
55
|
+
queries. `alarm.watch()` yields JSON-friendly alarm and connection events. See
|
|
56
|
+
`AlarmClient` in `tecnoctl/client.py`.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
python3 -m unittest discover -s tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
MIT licensed. Tecnoalarm is a trademark of its owner; this project is not
|
|
65
|
+
affiliated with or endorsed by Tecnoalarm.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tecnoctl"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Unofficial client for the myTecnoalarm direct TCP protocol"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["cryptography>=47", "python-dotenv>=1"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
tecnoctl = "tecnoctl.cli:main"
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Repository = "https://github.com/EnricoDev1/tecnoctl"
|
|
19
|
+
Issues = "https://github.com/EnricoDev1/tecnoctl/issues"
|
tecnoctl-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Command-line interface for :class:`tecnoctl.AlarmClient`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import getpass
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
from .client import AlarmClient, ProtocolError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _one_based(value):
|
|
18
|
+
try:
|
|
19
|
+
number = int(value)
|
|
20
|
+
except ValueError as exc:
|
|
21
|
+
raise argparse.ArgumentTypeError("must be an integer") from exc
|
|
22
|
+
if not 1 <= number <= 65536:
|
|
23
|
+
raise argparse.ArgumentTypeError("must be between 1 and 65536")
|
|
24
|
+
return number - 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _app_id(value):
|
|
28
|
+
try:
|
|
29
|
+
number = int(value, 0)
|
|
30
|
+
except ValueError as exc:
|
|
31
|
+
raise argparse.ArgumentTypeError(
|
|
32
|
+
"must be a decimal or 0x-prefixed integer"
|
|
33
|
+
) from exc
|
|
34
|
+
if not 0 <= number <= 0xFFFF:
|
|
35
|
+
raise argparse.ArgumentTypeError("must fit in 16 bits")
|
|
36
|
+
return number
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _positive(value):
|
|
40
|
+
try:
|
|
41
|
+
number = int(value)
|
|
42
|
+
except ValueError as exc:
|
|
43
|
+
raise argparse.ArgumentTypeError("must be an integer") from exc
|
|
44
|
+
if number < 1:
|
|
45
|
+
raise argparse.ArgumentTypeError("must be at least 1")
|
|
46
|
+
return number
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _nonnegative(value):
|
|
50
|
+
try:
|
|
51
|
+
number = int(value)
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise argparse.ArgumentTypeError("must be an integer") from exc
|
|
54
|
+
if number < 0:
|
|
55
|
+
raise argparse.ArgumentTypeError("cannot be negative")
|
|
56
|
+
return number
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _watch_interval(value):
|
|
60
|
+
try:
|
|
61
|
+
number = float(value)
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
raise argparse.ArgumentTypeError("must be a number") from exc
|
|
64
|
+
if not 5 <= number < float("inf"):
|
|
65
|
+
raise argparse.ArgumentTypeError("must be at least 5 seconds")
|
|
66
|
+
return number
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _clear_status_line():
|
|
70
|
+
if sys.stderr.isatty():
|
|
71
|
+
print("\r\033[2K", end="", file=sys.stderr, flush=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _countdown(seconds):
|
|
75
|
+
if not sys.stderr.isatty():
|
|
76
|
+
time.sleep(seconds)
|
|
77
|
+
return
|
|
78
|
+
deadline = time.monotonic() + seconds
|
|
79
|
+
while (remaining := deadline - time.monotonic()) > 0:
|
|
80
|
+
print(
|
|
81
|
+
f"\r\033[2Knext request in {math.ceil(remaining)} s",
|
|
82
|
+
end="",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
flush=True,
|
|
85
|
+
)
|
|
86
|
+
time.sleep(min(1, remaining))
|
|
87
|
+
_clear_status_line()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parser():
|
|
91
|
+
parser = argparse.ArgumentParser(
|
|
92
|
+
description="Control a Tecnoalarm panel over direct TCP"
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument("host", help="panel hostname or IP address")
|
|
95
|
+
parser.add_argument("--port", type=int, default=10001)
|
|
96
|
+
parser.add_argument("--timeout", type=float, default=10.0)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--debug", action="store_true", help="log connection and protocol diagnostics"
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"-v", "--verbose", action="store_true", help="log connection status"
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--app-id",
|
|
105
|
+
type=_app_id,
|
|
106
|
+
help="16-bit client ID (default: stable value derived from this machine)",
|
|
107
|
+
)
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--passphrase", help="network passphrase (prefer TECNOCTL_PASSPHRASE)"
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--code", help="4-6 digit user code (prefer TECNOCTL_CODE)"
|
|
113
|
+
)
|
|
114
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
115
|
+
commands.add_parser("info", help="panel model, limits, session, and raw information")
|
|
116
|
+
commands.add_parser("clock", help="raw panel clock record")
|
|
117
|
+
commands.add_parser("status", help="complete panel/program/remote status")
|
|
118
|
+
commands.add_parser("panel-status", help="decoded central-unit status and all flags")
|
|
119
|
+
commands.add_parser("group-status", help="program and remote status without names")
|
|
120
|
+
commands.add_parser("permissions", help="program and remote permissions for this code")
|
|
121
|
+
commands.add_parser("programs", help="program IDs, names, permissions, and states")
|
|
122
|
+
commands.add_parser("remotes", help="remote-control IDs, names, permissions, and states")
|
|
123
|
+
zones = commands.add_parser("zones", help="zone IDs, names, and states")
|
|
124
|
+
zones.add_argument("--start", type=_one_based, default=0, metavar="ZONE")
|
|
125
|
+
zones.add_argument("--count", type=_positive)
|
|
126
|
+
zones.add_argument("--filter", choices=("all", "open", "isolated"), default="all")
|
|
127
|
+
code_names = commands.add_parser("code-names", help="code labels only; never PIN digits")
|
|
128
|
+
code_names.add_argument("--start", type=_one_based, default=0, metavar="CODE")
|
|
129
|
+
code_names.add_argument("--count", type=_positive)
|
|
130
|
+
events = commands.add_parser("events", help="event log; use --limit 0 for all")
|
|
131
|
+
events.add_argument("--limit", type=_nonnegative, default=50)
|
|
132
|
+
watch = commands.add_parser(
|
|
133
|
+
"watch", help="print alarm and program-state events as JSON lines"
|
|
134
|
+
)
|
|
135
|
+
watch.add_argument("--interval", type=_watch_interval, default=30.0)
|
|
136
|
+
watch.add_argument(
|
|
137
|
+
"--debug",
|
|
138
|
+
action="store_true",
|
|
139
|
+
default=argparse.SUPPRESS,
|
|
140
|
+
help="log connection, protocol, and poll diagnostics",
|
|
141
|
+
)
|
|
142
|
+
watch.add_argument(
|
|
143
|
+
"-v",
|
|
144
|
+
"--verbose",
|
|
145
|
+
action="store_true",
|
|
146
|
+
default=argparse.SUPPRESS,
|
|
147
|
+
help="log connection status and meaningful poll activity",
|
|
148
|
+
)
|
|
149
|
+
commands.add_parser("sync", help="full configuration and status synchronization")
|
|
150
|
+
open_zones = commands.add_parser("open-zones", help="open zones blocking a program")
|
|
151
|
+
open_zones.add_argument("program", type=_one_based)
|
|
152
|
+
arm = commands.add_parser("arm", help="arm a program")
|
|
153
|
+
arm.add_argument("program", type=_one_based)
|
|
154
|
+
arm.add_argument("--exclude-open", action="store_true")
|
|
155
|
+
disarm = commands.add_parser("disarm", help="disarm a program")
|
|
156
|
+
disarm.add_argument("program", type=_one_based)
|
|
157
|
+
remote_on = commands.add_parser("remote-on", help="turn a remote control on")
|
|
158
|
+
remote_on.add_argument("remote", type=_one_based)
|
|
159
|
+
remote_off = commands.add_parser("remote-off", help="turn a remote control off")
|
|
160
|
+
remote_off.add_argument("remote", type=_one_based)
|
|
161
|
+
zone_status = commands.add_parser("zone-status", help="read one zone")
|
|
162
|
+
zone_status.add_argument("zone", type=_one_based)
|
|
163
|
+
isolate = commands.add_parser("isolate", help="isolate a zone (master code required)")
|
|
164
|
+
isolate.add_argument("zone", type=_one_based)
|
|
165
|
+
reintegrate = commands.add_parser(
|
|
166
|
+
"reintegrate", help="reintegrate a zone (master code required)"
|
|
167
|
+
)
|
|
168
|
+
reintegrate.add_argument("zone", type=_one_based)
|
|
169
|
+
return parser
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
load_dotenv(".env")
|
|
174
|
+
parser = _parser()
|
|
175
|
+
args = parser.parse_args()
|
|
176
|
+
log_level = logging.DEBUG if args.debug else logging.INFO if args.verbose else None
|
|
177
|
+
if log_level is not None:
|
|
178
|
+
logging.basicConfig(
|
|
179
|
+
level=log_level,
|
|
180
|
+
format="%(asctime)s %(levelname)s %(message)s",
|
|
181
|
+
datefmt="%H:%M:%S",
|
|
182
|
+
)
|
|
183
|
+
passphrase = args.passphrase or os.getenv("TECNOCTL_PASSPHRASE")
|
|
184
|
+
code = args.code or os.getenv("TECNOCTL_CODE")
|
|
185
|
+
if passphrase is None:
|
|
186
|
+
passphrase = getpass.getpass("Network passphrase: ")
|
|
187
|
+
if code is None:
|
|
188
|
+
code = getpass.getpass("Access code: ")
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
client = AlarmClient(
|
|
192
|
+
args.host,
|
|
193
|
+
passphrase,
|
|
194
|
+
code,
|
|
195
|
+
port=args.port,
|
|
196
|
+
app_id=args.app_id,
|
|
197
|
+
timeout=args.timeout,
|
|
198
|
+
)
|
|
199
|
+
if args.command == "watch":
|
|
200
|
+
print(
|
|
201
|
+
f"watching {args.host}:{args.port} every {args.interval:g} seconds; "
|
|
202
|
+
"Ctrl-C to stop",
|
|
203
|
+
file=sys.stderr,
|
|
204
|
+
)
|
|
205
|
+
try:
|
|
206
|
+
for event in client.watch(args.interval, wait=_countdown):
|
|
207
|
+
_clear_status_line()
|
|
208
|
+
print(json.dumps(event), flush=True)
|
|
209
|
+
except KeyboardInterrupt:
|
|
210
|
+
pass
|
|
211
|
+
finally:
|
|
212
|
+
_clear_status_line()
|
|
213
|
+
client.close()
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
with client as alarm:
|
|
217
|
+
if args.command == "info":
|
|
218
|
+
result = alarm.panel_info()
|
|
219
|
+
elif args.command == "clock":
|
|
220
|
+
result = {"raw": alarm.clock.hex()}
|
|
221
|
+
elif args.command == "status":
|
|
222
|
+
result = alarm.status()
|
|
223
|
+
elif args.command == "panel-status":
|
|
224
|
+
result = alarm.panel_status()
|
|
225
|
+
elif args.command == "group-status":
|
|
226
|
+
result = alarm.group_status()
|
|
227
|
+
elif args.command == "permissions":
|
|
228
|
+
result = alarm.permissions()
|
|
229
|
+
elif args.command == "programs":
|
|
230
|
+
result = alarm.programs()
|
|
231
|
+
elif args.command == "remotes":
|
|
232
|
+
result = alarm.remotes()
|
|
233
|
+
elif args.command == "zones":
|
|
234
|
+
result = alarm.zones(args.start, args.count)
|
|
235
|
+
if args.filter != "all":
|
|
236
|
+
result = [zone for zone in result if zone[args.filter]]
|
|
237
|
+
elif args.command == "code-names":
|
|
238
|
+
result = alarm.code_names(args.start, args.count)
|
|
239
|
+
elif args.command == "events":
|
|
240
|
+
result = alarm.events(args.limit)
|
|
241
|
+
elif args.command == "sync":
|
|
242
|
+
result = alarm.sync()
|
|
243
|
+
elif args.command == "open-zones":
|
|
244
|
+
result = {"zones": [zone + 1 for zone in alarm.open_zones(args.program)]}
|
|
245
|
+
else:
|
|
246
|
+
_run_action(alarm, args)
|
|
247
|
+
return
|
|
248
|
+
print(json.dumps(result, indent=2))
|
|
249
|
+
except (OSError, ProtocolError, ValueError) as exc:
|
|
250
|
+
parser.exit(1, f"error: {exc}\n")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_action(alarm, args):
|
|
254
|
+
if args.command == "arm":
|
|
255
|
+
excluded = alarm.arm(args.program, args.exclude_open)
|
|
256
|
+
suffix = (
|
|
257
|
+
f"; excluded zones {', '.join(str(zone + 1) for zone in excluded)}"
|
|
258
|
+
if excluded
|
|
259
|
+
else ""
|
|
260
|
+
)
|
|
261
|
+
print(f"program {args.program + 1} armed{suffix}")
|
|
262
|
+
elif args.command == "disarm":
|
|
263
|
+
alarm.disarm(args.program)
|
|
264
|
+
print(f"program {args.program + 1} disarmed")
|
|
265
|
+
elif args.command in ("remote-on", "remote-off"):
|
|
266
|
+
enabled = args.command == "remote-on"
|
|
267
|
+
alarm.remote(args.remote, enabled)
|
|
268
|
+
print(f"remote {args.remote + 1} {'on' if enabled else 'off'}")
|
|
269
|
+
elif args.command == "zone-status":
|
|
270
|
+
print(json.dumps(alarm.zone_status(args.zone), indent=2))
|
|
271
|
+
else:
|
|
272
|
+
isolated = args.command == "isolate"
|
|
273
|
+
alarm.set_zone_isolation(args.zone, isolated)
|
|
274
|
+
print(f"zone {args.zone + 1} {'isolated' if isolated else 'reintegrated'}")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|