mirror of https://github.com/OpenIPC/python-dvr
29 changed files with 5020 additions and 23 deletions
@ -0,0 +1,77 @@ |
|||
# For most projects, this workflow file will not need changing; you simply need |
|||
# to commit it to your repository. |
|||
# |
|||
# You may wish to alter this file to override the set of languages analyzed, |
|||
# or to provide custom queries or build logic. |
|||
# |
|||
# ******** NOTE ******** |
|||
# We have attempted to detect the languages in your repository. Please check |
|||
# the `language` matrix defined below to confirm you have the correct set of |
|||
# supported CodeQL languages. |
|||
# |
|||
name: "CodeQL" |
|||
|
|||
on: |
|||
push: |
|||
branches: [ "master" ] |
|||
pull_request: |
|||
# The branches below must be a subset of the branches above |
|||
branches: [ "master" ] |
|||
schedule: |
|||
- cron: '15 1 * * 2' |
|||
|
|||
jobs: |
|||
analyze: |
|||
name: Analyze |
|||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} |
|||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} |
|||
permissions: |
|||
actions: read |
|||
contents: read |
|||
security-events: write |
|||
|
|||
strategy: |
|||
fail-fast: false |
|||
matrix: |
|||
language: [ 'python' ] |
|||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] |
|||
# Use only 'java' to analyze code written in Java, Kotlin or both |
|||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both |
|||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support |
|||
|
|||
steps: |
|||
- name: Checkout repository |
|||
uses: actions/checkout@v3 |
|||
|
|||
# Initializes the CodeQL tools for scanning. |
|||
- name: Initialize CodeQL |
|||
uses: github/codeql-action/init@v2 |
|||
with: |
|||
languages: ${{ matrix.language }} |
|||
# If you wish to specify custom queries, you can do so here or in a config file. |
|||
# By default, queries listed here will override any specified in a config file. |
|||
# Prefix the list here with "+" to use these queries and those in the config file. |
|||
|
|||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs |
|||
# queries: security-extended,security-and-quality |
|||
|
|||
|
|||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). |
|||
# If this step fails, then you should remove it and run the build manually (see below) |
|||
- name: Autobuild |
|||
uses: github/codeql-action/autobuild@v2 |
|||
|
|||
# ℹ️ Command-line programs to run using the OS shell. |
|||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun |
|||
|
|||
# If the Autobuild fails above, remove it and uncomment the following three lines. |
|||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. |
|||
|
|||
# - run: | |
|||
# echo "Run, Build Application using script" |
|||
# ./location_of_script_within_repo/buildscript.sh |
|||
|
|||
- name: Perform CodeQL Analysis |
|||
uses: github/codeql-action/analyze@v2 |
|||
with: |
|||
category: "/language:${{matrix.language}}" |
@ -0,0 +1,24 @@ |
|||
name: ci |
|||
on: |
|||
push: |
|||
branches: |
|||
- "*" |
|||
workflow_dispatch: |
|||
jobs: |
|||
docker: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Set up QEMU |
|||
uses: docker/setup-qemu-action@v1 |
|||
- name: Set up Docker Buildx |
|||
uses: docker/setup-buildx-action@v1 |
|||
- name: Login to DockerHub |
|||
uses: docker/login-action@v1 |
|||
with: |
|||
username: ${{ secrets.DOCKERHUB_USERNAME }} |
|||
password: ${{ secrets.DOCKERHUB_TOKEN }} |
|||
- name: Build and push |
|||
uses: docker/build-push-action@v2 |
|||
with: |
|||
push: true |
|||
tags: braunbearded/python-dvr:latest,braunbearded/python-dvr:${{ github.sha }} |
@ -0,0 +1,3 @@ |
|||
*.pyc |
|||
*.old |
|||
.DS_Store |
@ -0,0 +1,55 @@ |
|||
#!/usr/bin/env python |
|||
# -*- coding: utf-8 -*- |
|||
|
|||
import os, sys, struct, json |
|||
from time import sleep |
|||
from socket import * |
|||
from datetime import * |
|||
|
|||
if len(sys.argv) > 1: |
|||
port = sys.argv[1] |
|||
else: |
|||
print("Usage: %s [Port]" % os.path.basename(sys.argv[0])) |
|||
port = input("Port(default 15002): ") |
|||
if port == "": |
|||
port = "15002" |
|||
server = socket(AF_INET, SOCK_STREAM) |
|||
server.bind(("0.0.0.0", int(port))) |
|||
# server.settimeout(0.5) |
|||
server.listen(1) |
|||
|
|||
log = "info.txt" |
|||
|
|||
|
|||
def tolog(s): |
|||
logfile = open(datetime.now().strftime("%Y_%m_%d_") + log, "a+") |
|||
logfile.write(s) |
|||
logfile.close() |
|||
|
|||
|
|||
def GetIP(s): |
|||
return inet_ntoa(struct.pack("<I", int(s, 16))) |
|||
|
|||
|
|||
while True: |
|||
try: |
|||
conn, addr = server.accept() |
|||
head, version, session, sequence_number, msgid, len_data = struct.unpack( |
|||
"BB2xII2xHI", conn.recv(20) |
|||
) |
|||
sleep(0.1) # Just for recive whole packet |
|||
data = conn.recv(len_data) |
|||
conn.close() |
|||
reply = json.loads(data, encoding="utf8") |
|||
print(datetime.now().strftime("[%Y-%m-%d %H:%M:%S]>>>")) |
|||
print(head, version, session, sequence_number, msgid, len_data) |
|||
print(json.dumps(reply, indent=4, sort_keys=True)) |
|||
print("<<<") |
|||
tolog(repr(data) + "\r\n") |
|||
except (KeyboardInterrupt, SystemExit): |
|||
break |
|||
# except: |
|||
# e = 1 |
|||
# print "no" |
|||
server.close() |
|||
sys.exit(1) |
@ -0,0 +1,99 @@ |
|||
//заготовки
|
|||
//'{"EncryptType": "MD5", "LoginType": "DVRIP-Web", "PassWord": "00000000", "UserName": "admin"}'
|
|||
char login_packet_bytes[] = { |
|||
0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
|||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x03, |
|||
0x5f, 0x00, 0x00, 0x00, 0x7b, 0x22, 0x45, 0x6e, |
|||
0x63, 0x72, 0x79, 0x70, 0x74, 0x54, 0x79, 0x70, |
|||
0x65, 0x22, 0x3a, 0x20, 0x22, 0x4d, 0x44, 0x35, |
|||
0x22, 0x2c, 0x20, 0x22, 0x4c, 0x6f, 0x67, 0x69, |
|||
0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, |
|||
0x22, 0x44, 0x56, 0x52, 0x49, 0x50, 0x2d, 0x57, |
|||
0x65, 0x62, 0x22, 0x2c, 0x20, 0x22, 0x50, 0x61, |
|||
0x73, 0x73, 0x57, 0x6f, 0x72, 0x64, 0x22, 0x3a, |
|||
0x20, 0x22, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, |
|||
0x30, 0x30, 0x22, 0x2c, 0x20, 0x22, 0x55, 0x73, |
|||
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, |
|||
0x20, 0x22, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x22, |
|||
0x7d, 0x0a, 0x00 |
|||
}; |
|||
//'{"Name": "fVideo.OSDInfo", "SessionID": "0x00000002", "fVideo.OSDInfo": {"OSDInfo": [{"Info": ["0", "0", "0"], "OSDInfoWidget": {"BackColor": "0x00000000", "EncodeBlend": true, "FrontColor": "0xF0FFFFFF", "PreviewBlend": true, "RelativePos": [6144, 6144, 8192, 8192]}}], "strEnc": "UTF-8"}}'
|
|||
char set_packet_bytes[] = { |
|||
0xff, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, |
|||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x04, |
|||
0x24, 0x01, 0x00, 0x00, 0x7b, 0x22, 0x4e, 0x61, |
|||
0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x66, 0x56, |
|||
0x69, 0x64, 0x65, 0x6f, 0x2e, 0x4f, 0x53, 0x44, |
|||
0x49, 0x6e, 0x66, 0x6f, 0x22, 0x2c, 0x20, 0x22, |
|||
0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, |
|||
0x44, 0x22, 0x3a, 0x20, 0x22, 0x30, 0x78, 0x30, |
|||
0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x32, 0x22, |
|||
0x2c, 0x20, 0x22, 0x66, 0x56, 0x69, 0x64, 0x65, |
|||
0x6f, 0x2e, 0x4f, 0x53, 0x44, 0x49, 0x6e, 0x66, |
|||
0x6f, 0x22, 0x3a, 0x20, 0x7b, 0x22, 0x4f, 0x53, |
|||
0x44, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x3a, 0x20, |
|||
0x5b, 0x7b, 0x22, 0x49, 0x6e, 0x66, 0x6f, 0x22, |
|||
0x3a, 0x20, 0x5b, 0x22, 0x30, 0x22, 0x2c, 0x20, |
|||
0x22, 0x30, 0x22, 0x2c, 0x20, 0x22, 0x30, 0x22, |
|||
0x5d, 0x2c, 0x20, 0x22, 0x4f, 0x53, 0x44, 0x49, |
|||
0x6e, 0x66, 0x6f, 0x57, 0x69, 0x64, 0x67, 0x65, |
|||
0x74, 0x22, 0x3a, 0x20, 0x7b, 0x22, 0x42, 0x61, |
|||
0x63, 0x6b, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x22, |
|||
0x3a, 0x20, 0x22, 0x30, 0x78, 0x30, 0x30, 0x30, |
|||
0x30, 0x30, 0x30, 0x30, 0x30, 0x22, 0x2c, 0x20, |
|||
0x22, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x65, 0x42, |
|||
0x6c, 0x65, 0x6e, 0x64, 0x22, 0x3a, 0x20, 0x74, |
|||
0x72, 0x75, 0x65, 0x2c, 0x20, 0x22, 0x46, 0x72, |
|||
0x6f, 0x6e, 0x74, 0x43, 0x6f, 0x6c, 0x6f, 0x72, |
|||
0x22, 0x3a, 0x20, 0x22, 0x30, 0x78, 0x46, 0x30, |
|||
0x46, 0x46, 0x46, 0x46, 0x46, 0x46, 0x22, 0x2c, |
|||
0x20, 0x22, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, |
|||
0x77, 0x42, 0x6c, 0x65, 0x6e, 0x64, 0x22, 0x3a, |
|||
0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x20, 0x22, |
|||
0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, |
|||
0x50, 0x6f, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x36, |
|||
0x31, 0x34, 0x34, 0x2c, 0x20, 0x36, 0x31, 0x34, |
|||
0x34, 0x2c, 0x20, 0x38, 0x31, 0x39, 0x32, 0x2c, |
|||
0x20, 0x38, 0x31, 0x39, 0x32, 0x5d, 0x7d, 0x7d, |
|||
0x5d, 0x2c, 0x20, 0x22, 0x73, 0x74, 0x72, 0x45, |
|||
0x6e, 0x63, 0x22, 0x3a, 0x20, 0x22, 0x55, 0x54, |
|||
0x46, 0x2d, 0x38, 0x22, 0x7d, 0x7d, 0x0a, 0x00 |
|||
}; |
|||
|
|||
char str1[] = "Test: 1"; |
|||
char str2[] = "Test: 2"; |
|||
char str3[] = "Test: 3"; |
|||
|
|||
memcpy( &login_packet_bytes[83], "00000000", 8 );//set password hash(83..88)
|
|||
client.write(login_packet_bytes); |
|||
char income[20] = client.read(20) |
|||
int len = 289+sizeof(str1)+sizeof(str2)+sizeof(str3); |
|||
char buff[len]; |
|||
int offset = 0; |
|||
memcpy( &buff[4], $income[4], 4 );//4...7 - session id
|
|||
memcpy( &buff[16], &len, 2);//set len 16..17 - bytes
|
|||
//TO DO: set session hex str
|
|||
//70...63 - hex string session
|
|||
memcpy( &buff[offset], &set_packet_bytes[0], 116); |
|||
//116 str1
|
|||
//121 str2
|
|||
//126 str3
|
|||
offset += 116; |
|||
memcpy( &buff[offset], &str1[0], sizeof(str1));//set str1
|
|||
offset +=sizeof(str1); |
|||
memcpy( &buff[offset], &set_packet_bytes[117], 4); |
|||
offset += 4; |
|||
memcpy( &buff[offset], &str2[0], sizeof(str2));//set str2
|
|||
offset += sizeof(str2); |
|||
memcpy( &buff[offset], &set_packet_bytes[117], 4); |
|||
offset += 4; |
|||
memcpy( &buff[offset], &str3[0], sizeof(str3));//set str3
|
|||
offset += sizeof(str3); |
|||
memcpy( &buff[offset], &set_packet_bytes[127], 185); |
|||
offset += 38; |
|||
memcpy( &buff[offset], "00000000", 8 );//BG color
|
|||
offset += 41; |
|||
memcpy( &buff[offset], "F0FFFFFF", 8 );//FG color
|
|||
//Serial.write(buff);//debug
|
|||
client.write(buff); |
|||
client.close(); |
File diff suppressed because it is too large
@ -0,0 +1,12 @@ |
|||
FROM python:slim |
|||
|
|||
RUN apt-get update && \ |
|||
apt-get upgrade -y && \ |
|||
apt-get install -y \ |
|||
ffmpeg |
|||
|
|||
WORKDIR /app |
|||
|
|||
COPY . . |
|||
|
|||
CMD [ "python3", "./download-local-files.py"] |
@ -1,21 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2023 OpenIPC |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
MIT License |
|||
|
|||
Copyright (c) 2017 Eliot Kent Woodrich |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
|
@ -1,2 +1,703 @@ |
|||
# python-dvr |
|||
python-dvr |
|||
# python-dvr |
|||
|
|||
Python library for configuring a wide range of IP cameras that use the NETsurveillance ActiveX plugin |
|||
XMeye SDK |
|||
|
|||
 |
|||
|
|||
## DeviceManager.py |
|||
|
|||
DeviceManager.py is a standalone Tkinter and console interface program such as the original DeviceManager.exe |
|||
it possible to work on both systems, if there is no Tkinter it starts with a console interface |
|||
|
|||
## DVR-IP, NetSurveillance or "Sofia" Protocol |
|||
|
|||
The NETSurveillance ActiveX plugin uses a TCP based protocol referred to simply as the "Digital Video Recorder Interface Protocol" by the "Hangzhou male Mai Information Co". |
|||
|
|||
There is very little software support or documentation other than through tools provided by the manufacturers of these cameras, which leaves many configuration options inaccessible. |
|||
|
|||
- [Command and response codes](https://gist.github.com/ekwoodrich/a6d7b8db8f82adf107c3c366e61fd36f) |
|||
|
|||
- [Xiongmai DVR API v1.0, Russian](https://github.com/NeiroNx/python-dvr/blob/master/doc/%D0%A1%D0%BE%D0%B3%D0%BB%D0%B0%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BE%20%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81%D0%B5%20%D1%86%D0%B8%D1%84%D1%80%D0%BE%D0%B2%D0%BE%D0%B3%D0%BE%20%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE%D1%80%D0%B5%D0%B3%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%B0%20XiongmaiV1.0.doc?raw=true) |
|||
|
|||
- [Xiongmai DVR API, 2013-01-11, Chinese](doc/雄迈数字视频录像机接口协议_V1.0.0.pdf) |
|||
|
|||
- [DVR API, brief description, Chinese](doc/配置交换格式V2.0.pdf) |
|||
|
|||
- [NETIP video/audio payload protocol, Chinese](doc/码流帧格式文档.pdf) |
|||
|
|||
### Similar projects |
|||
|
|||
- [sofiactl](https://github.com/667bdrm/sofiactl) |
|||
|
|||
- [DVRIP library and tools](https://github.com/alexshpilkin/dvrip) |
|||
|
|||
- [numenworld-ipcam](https://github.com/johndoe31415/numenworld-ipcam) |
|||
|
|||
### Server implementations |
|||
|
|||
* [OpenIPC](https://openipc.org/firmware/) |
|||
|
|||
## Basic usage |
|||
|
|||
```python |
|||
from dvrip import DVRIPCam |
|||
from time import sleep |
|||
|
|||
host_ip = '192.168.1.10' |
|||
|
|||
cam = DVRIPCam(host_ip, user='admin', password='') |
|||
if cam.login(): |
|||
print("Success! Connected to " + host_ip) |
|||
else: |
|||
print("Failure. Could not connect.") |
|||
|
|||
print("Camera time:", cam.get_time()) |
|||
|
|||
# Reboot camera |
|||
cam.reboot() |
|||
sleep(60) # wait while camera starts |
|||
|
|||
# Login again |
|||
cam.login() |
|||
# Sync camera time with PC time |
|||
cam.set_time() |
|||
# Disconnect |
|||
cam.close() |
|||
``` |
|||
|
|||
## AsyncIO usage |
|||
```python |
|||
from asyncio_dvrip import DVRIPCam |
|||
import asyncio |
|||
import traceback |
|||
|
|||
def stop(loop): |
|||
tasks = asyncio.gather(*asyncio.Task.all_tasks(loop=loop), loop=loop, return_exceptions=True) |
|||
tasks.add_done_callback(lambda t: loop.stop()) |
|||
tasks.cancel() |
|||
|
|||
loop = asyncio.get_event_loop() |
|||
|
|||
def onAlert(event, sequence_number): |
|||
print(event, sequence_number) |
|||
|
|||
async def some_test_worker(): |
|||
while True: |
|||
print("do some important work...") |
|||
|
|||
await asyncio.sleep(3) |
|||
|
|||
async def main(loop): |
|||
host_ip = '192.168.1.10' |
|||
cam = DVRIPCam(host_ip, user='admin', password='') |
|||
try: |
|||
if not await cam.login(): |
|||
raise Exception("Failure. Could not connect.") |
|||
|
|||
# ------------------------------- |
|||
|
|||
# take snapshot |
|||
image = await cam.snapshot() |
|||
# save it |
|||
with open("snap.jpeg", "wb") as fp: |
|||
fp.write(image) |
|||
|
|||
# ------------------------------- |
|||
|
|||
# write video |
|||
with open("datastream.h265", "wb") as f: |
|||
await cam.start_monitor(lambda frame, meta, user: f.write(frame)) |
|||
|
|||
# ------------------------------- |
|||
|
|||
# or get alarms |
|||
cam.setAlarm(onAlert) |
|||
# will create new task |
|||
await cam.alarmStart(loop) |
|||
|
|||
# so just wait or something else |
|||
while True: |
|||
await asyncio.sleep(1) |
|||
|
|||
# ------------------------------- |
|||
|
|||
except: |
|||
pass |
|||
finally: |
|||
cam.close() |
|||
|
|||
try: |
|||
loop.create_task(main(loop)) |
|||
loop.create_task(some_test_worker()) |
|||
|
|||
loop.run_forever() |
|||
except Exception as err: |
|||
msg = ''.join(traceback.format_tb(err.__traceback__) + [str(err)]) |
|||
print(msg) |
|||
finally: |
|||
cam.close() |
|||
stop(loop) |
|||
``` |
|||
|
|||
## Camera settings |
|||
|
|||
```python |
|||
params = cam.get_general_info() |
|||
``` |
|||
|
|||
Returns general camera information (timezones, formats, auto reboot policy, |
|||
security options): |
|||
|
|||
```json |
|||
{ |
|||
"AppBindFlag": { |
|||
"BeBinded": false |
|||
}, |
|||
"AutoMaintain": { |
|||
"AutoDeleteFilesDays": 0, |
|||
"AutoRebootDay": "Tuesday", |
|||
"AutoRebootHour": 3 |
|||
}, |
|||
"DSTState": { |
|||
"InNormalState": true |
|||
}, |
|||
"General": { |
|||
"AutoLogout": 0, |
|||
"FontSize": 24, |
|||
"IranCalendarEnable": 0, |
|||
"LocalNo": 0, |
|||
"MachineName": "LocalHost", |
|||
"OverWrite": "OverWrite", |
|||
"ScreenAutoShutdown": 10, |
|||
"ScreenSaveTime": 0, |
|||
"VideoOutPut": "Auto" |
|||
}, |
|||
"Location": { |
|||
"DSTEnd": { |
|||
"Day": 1, |
|||
"Hour": 1, |
|||
"Minute": 1, |
|||
"Month": 10, |
|||
"Week": 0, |
|||
"Year": 2021 |
|||
}, |
|||
"DSTRule": "Off", |
|||
"DSTStart": { |
|||
"Day": 1, |
|||
"Hour": 1, |
|||
"Minute": 1, |
|||
"Month": 5, |
|||
"Week": 0, |
|||
"Year": 2021 |
|||
}, |
|||
"DateFormat": "YYMMDD", |
|||
"DateSeparator": "-", |
|||
"IranCalendar": 0, |
|||
"Language": "Russian", |
|||
"TimeFormat": "24", |
|||
"VideoFormat": "PAL", |
|||
"Week": null, |
|||
"WorkDay": 62 |
|||
}, |
|||
"OneKeyMaskVideo": null, |
|||
"PwdSafety": { |
|||
"PwdReset": [ |
|||
{ |
|||
"QuestionAnswer": "", |
|||
"QuestionIndex": 0 |
|||
}, |
|||
{ |
|||
"QuestionAnswer": "", |
|||
"QuestionIndex": 0 |
|||
}, |
|||
{ |
|||
"QuestionAnswer": "", |
|||
"QuestionIndex": 0 |
|||
}, |
|||
{ |
|||
"QuestionAnswer": "", |
|||
"QuestionIndex": 0 |
|||
} |
|||
], |
|||
"SecurityEmail": "", |
|||
"TipPageHide": false |
|||
}, |
|||
"ResumePtzState": null, |
|||
"TimingSleep": null |
|||
} |
|||
``` |
|||
|
|||
```python |
|||
params = cam.get_system_info() |
|||
``` |
|||
|
|||
Returns hardware specific settings, camera serial number, current software |
|||
version and firmware type: |
|||
|
|||
```json |
|||
{ |
|||
"AlarmInChannel": 2, |
|||
"AlarmOutChannel": 1, |
|||
"AudioInChannel": 1, |
|||
"BuildTime": "2020-01-08 11:05:18", |
|||
"CombineSwitch": 0, |
|||
"DeviceModel": "HI3516EV300_85H50AI", |
|||
"DeviceRunTime": "0x0001f532", |
|||
"DigChannel": 0, |
|||
"EncryptVersion": "Unknown", |
|||
"ExtraChannel": 0, |
|||
"HardWare": "HI3516EV300_85H50AI", |
|||
"HardWareVersion": "Unknown", |
|||
"SerialNo": "a166379674a3b447", |
|||
"SoftWareVersion": "V5.00.R02.000529B2.10010.040600.0020000", |
|||
"TalkInChannel": 1, |
|||
"TalkOutChannel": 1, |
|||
"UpdataTime": "", |
|||
"UpdataType": "0x00000000", |
|||
"VideoInChannel": 1, |
|||
"VideoOutChannel": 1 |
|||
} |
|||
``` |
|||
|
|||
```python |
|||
params = cam.get_system_capabilities() |
|||
``` |
|||
|
|||
Returns capabilities for the camera software (alarms and detection, |
|||
communication protocols and hardware specific features): |
|||
|
|||
```json |
|||
{ |
|||
"AlarmFunction": { |
|||
"AlarmConfig": true, |
|||
"BlindDetect": true, |
|||
"HumanDection": true, |
|||
"HumanPedDetection": true, |
|||
"LossDetect": true, |
|||
"MotionDetect": true, |
|||
"NetAbort": true, |
|||
"NetAlarm": true, |
|||
"NetIpConflict": true, |
|||
"NewVideoAnalyze": false, |
|||
"PEAInHumanPed": true, |
|||
"StorageFailure": true, |
|||
"StorageLowSpace": true, |
|||
"StorageNotExist": true, |
|||
"VideoAnalyze": false |
|||
}, |
|||
"CommFunction": { |
|||
"CommRS232": true, |
|||
"CommRS485": true |
|||
}, |
|||
"EncodeFunction": { |
|||
"DoubleStream": true, |
|||
"SmartH264": true, |
|||
"SmartH264V2": false, |
|||
"SnapStream": true |
|||
}, |
|||
"NetServerFunction": { |
|||
"IPAdaptive": true, |
|||
"Net3G": false, |
|||
"Net4GSignalLevel": false, |
|||
"NetAlarmCenter": true, |
|||
"NetDAS": false, |
|||
"NetDDNS": false, |
|||
"NetDHCP": true, |
|||
"NetDNS": true, |
|||
"NetEmail": true, |
|||
"NetFTP": true, |
|||
"NetIPFilter": true, |
|||
"NetMutlicast": false, |
|||
"NetNTP": true, |
|||
"NetNat": true, |
|||
"NetPMS": true, |
|||
"NetPMSV2": true, |
|||
"NetPPPoE": false, |
|||
"NetRTSP": true, |
|||
"NetSPVMN": false, |
|||
"NetUPNP": true, |
|||
"NetWifi": false, |
|||
"OnvifPwdCheckout": true, |
|||
"RTMP": false, |
|||
"WifiModeSwitch": false, |
|||
"WifiRouteSignalLevel": true |
|||
}, |
|||
"OtherFunction": { |
|||
"NOHDDRECORD": false, |
|||
"NoSupportSafetyQuestion": false, |
|||
"NotSupportAutoAndIntelligent": false, |
|||
"SupportAdminContactInfo": true, |
|||
"SupportAlarmRemoteCall": false, |
|||
"SupportAlarmVoiceTipInterval": true, |
|||
"SupportAlarmVoiceTips": true, |
|||
"SupportAlarmVoiceTipsType": true, |
|||
"SupportAppBindFlag": true, |
|||
"SupportBT": true, |
|||
"SupportBallTelescopic": false, |
|||
"SupportBoxCameraBulb": false, |
|||
"SupportCamareStyle": true, |
|||
"SupportCameraWhiteLight": false, |
|||
"SupportCfgCloudupgrade": true, |
|||
"SupportChangeLanguageNoReboot": true, |
|||
"SupportCloseVoiceTip": false, |
|||
"SupportCloudUpgrade": true, |
|||
"SupportCommDataUpload": true, |
|||
"SupportCorridorMode": false, |
|||
"SupportCustomizeLpRect": false, |
|||
"SupportDNChangeByImage": false, |
|||
"SupportDimenCode": true, |
|||
"SupportDoubleLightBoxCamera": false, |
|||
"SupportDoubleLightBulb": false, |
|||
"SupportElectronicPTZ": false, |
|||
"SupportFTPTest": true, |
|||
"SupportFaceDetectV2": false, |
|||
"SupportFaceRecognition": false, |
|||
"SupportMailTest": true, |
|||
"SupportMusicBulb433Pair": false, |
|||
"SupportMusicLightBulb": false, |
|||
"SupportNetWorkMode": false, |
|||
"SupportOSDInfo": false, |
|||
"SupportOneKeyMaskVideo": false, |
|||
"SupportPCSetDoubleLight": true, |
|||
"SupportPTZDirectionControl": false, |
|||
"SupportPTZTour": false, |
|||
"SupportPWDSafety": true, |
|||
"SupportParkingGuide": false, |
|||
"SupportPtz360Spin": false, |
|||
"SupportRPSVideo": false, |
|||
"SupportSetBrightness": false, |
|||
"SupportSetDetectTrackWatchPoint": false, |
|||
"SupportSetHardwareAbility": false, |
|||
"SupportSetPTZPresetAttribute": false, |
|||
"SupportSetVolume": true, |
|||
"SupportShowH265X": true, |
|||
"SupportSnapCfg": false, |
|||
"SupportSnapV2Stream": true, |
|||
"SupportSnapshotConfigV2": false, |
|||
"SupportSoftPhotosensitive": true, |
|||
"SupportStatusLed": false, |
|||
"SupportTextPassword": true, |
|||
"SupportTimeZone": true, |
|||
"SupportTimingSleep": false, |
|||
"SupportWebRTCModule": false, |
|||
"SupportWriteLog": true, |
|||
"SuppportChangeOnvifPort": true |
|||
}, |
|||
"PreviewFunction": { |
|||
"Talk": true, |
|||
"Tour": false |
|||
}, |
|||
"TipShow": { |
|||
"NoBeepTipShow": true |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Camera video settings/modes |
|||
|
|||
```python |
|||
params = cam.get_info("Camera") |
|||
# Returns data like this: |
|||
# {'ClearFog': [{'enable': 0, 'level': 50}], 'DistortionCorrect': {'Lenstype': 0, 'Version': 0}, |
|||
# 'FishLensParam': [{'CenterOffsetX': 300, 'CenterOffsetY': 300, 'ImageHeight': 720, |
|||
# 'ImageWidth': 1280, 'LensType': 0, 'PCMac': '000000000000', 'Radius': 300, 'Version': 1, |
|||
# 'ViewAngle': 0, 'ViewMode': 0, 'Zoom': 100}], 'FishViCut': [{'ImgHeight': 0, 'ImgWidth': 0, |
|||
# 'Xoffset': 0, 'Yoffset': 0}], 'Param': [{'AeSensitivity': 5, 'ApertureMode': '0x00000000', |
|||
# 'BLCMode': '0x00000000', 'DayNightColor': '0x00000000', 'Day_nfLevel': 3, 'DncThr': 30, |
|||
# 'ElecLevel': 50, 'EsShutter': '0x00000002', 'ExposureParam': {'LeastTime': '0x00000100', |
|||
# 'Level': 0, 'MostTime': '0x00010000'}, 'GainParam': {'AutoGain': 1, 'Gain': 50}, |
|||
# 'IRCUTMode': 0, 'IrcutSwap': 0, 'Night_nfLevel': 3, 'PictureFlip': '0x00000000', |
|||
# 'PictureMirror': '0x00000000', 'RejectFlicker': '0x00000000', 'WhiteBalance': '0x00000000'}], |
|||
# 'ParamEx': [{'AutomaticAdjustment': 3, 'BroadTrends': {'AutoGain': 0, 'Gain': 50}, |
|||
# 'CorridorMode': 0, 'ExposureTime': '0x100', 'LightRestrainLevel': 16, 'LowLuxMode': 0, |
|||
# 'PreventOverExpo': 0, 'SoftPhotosensitivecontrol': 0, 'Style': 'type1'}], 'WhiteLight': |
|||
# {'MoveTrigLight': {'Duration': 60, 'Level': 3}, 'WorkMode': 'Auto', 'WorkPeriod': |
|||
# {'EHour': 6, 'EMinute': 0, 'Enable': 1, 'SHour': 18, 'SMinute': 0}}} |
|||
|
|||
# Get current encoding settings |
|||
enc_info = cam.get_info("Simplify.Encode") |
|||
# Returns data like this: |
|||
# [{'ExtraFormat': {'AudioEnable': False, 'Video': {'BitRate': 552, 'BitRateControl': 'VBR', |
|||
# 'Compression': 'H.265', 'FPS': 20, 'GOP': 2, 'Quality': 3, 'Resolution': 'D1'}, |
|||
# 'VideoEnable': True}, 'MainFormat': {'AudioEnable': False, 'Video': {'BitRate': 2662, |
|||
# 'BitRateControl': 'VBR', 'Compression': 'H.265', 'FPS': 25, 'GOP': 2, 'Quality': 4, |
|||
# 'Resolution': '1080P'}, 'VideoEnable': True}}] |
|||
|
|||
# Change bitrate |
|||
NewBitrate = 7000 |
|||
enc_info[0]['MainFormat']['Video']['BitRate'] = NewBitrate |
|||
cam.set_info("Simplify.Encode", enc_info) |
|||
|
|||
# Get videochannel color parameters |
|||
colors = cam.get_info("AVEnc.VideoColor.[0]") |
|||
# Returns data like this: |
|||
# [{'Enable': True, 'TimeSection': '0 00:00:00-24:00:00', 'VideoColorParam': {'Acutance': 3848, |
|||
# 'Brightness': 50, 'Contrast': 50, 'Gain': 0, 'Hue': 50, 'Saturation': 50, 'Whitebalance': 128}}, |
|||
# {'Enable': False, 'TimeSection': '0 00:00:00-24:00:00', 'VideoColorParam': {'Acutance': 3848, |
|||
# 'Brightness': 50, 'Contrast': 50, 'Gain': 0, 'Hue': 50, 'Saturation': 50, 'Whitebalance': 128}}] |
|||
|
|||
# Change IR Cut |
|||
cam.set_info("Camera.Param.[0]", { "IrcutSwap" : 0 }) |
|||
|
|||
# Change WDR settings |
|||
WDR_mode = True |
|||
cam.set_info("Camera.ParamEx.[0]", { "BroadTrends" : { "AutoGain" : int(WDR_mode) } }) |
|||
|
|||
# Get network settings |
|||
net = cam.get_info("NetWork.NetCommon") |
|||
# Turn on adaptive IP mode |
|||
cam.set_info("NetWork.IPAdaptive", { "IPAdaptive": True }) |
|||
# Set camera hostname |
|||
cam.set_info("NetWork.NetCommon.HostName", "IVG-85HG50PYA-S") |
|||
# Set DHCP mode (turn on in this case) |
|||
dhcpst = cam.get_info("NetWork.NetDHCP") |
|||
dhcpst[0]['Enable'] = True |
|||
cam.set_info("NetWork.NetDHCP", dhcpst) |
|||
|
|||
# Enable/disable cloud support |
|||
cloudEnabled = False |
|||
cam.set_info("NetWork.Nat", { "NatEnable" : cloudEnabled }) |
|||
``` |
|||
|
|||
## Add user and change password |
|||
|
|||
```python |
|||
#User "test2" with pssword "123123" |
|||
cam.addUser("test2","123123") |
|||
#Bad password, change it |
|||
cam.changePasswd("321321",cam.sofia_hash("123123"),"test2") |
|||
#And delete user "test2" |
|||
if cam.delUser("test2"): |
|||
print("User deleted") |
|||
else: |
|||
print("Can not delete it") |
|||
#System users can not be deleted |
|||
if cam.delUser("admin"): |
|||
print("You do it! How?") |
|||
else: |
|||
print("It system reserved user") |
|||
``` |
|||
|
|||
## Investigate more settings |
|||
|
|||
Suggested approach will help understand connections between camera UI and API |
|||
settings. Fell free to send PR to the document to update information. |
|||
|
|||
```python |
|||
from deepdiff import DeepDiff |
|||
from pprint import pprint |
|||
|
|||
latest = None |
|||
while True: |
|||
current = cam.get_info("Camera") # or "General", "Simplify.Encode", "NetWork" |
|||
if latest: |
|||
diff = DeepDiff(current, latest) |
|||
if diff == {}: |
|||
print("Nothing changed") |
|||
else: |
|||
pprint(diff['values_changed'], indent = 2) |
|||
latest = current |
|||
input("Change camera setting via UI and then press Enter," |
|||
" or double Ctrl-C to exit\n") |
|||
``` |
|||
|
|||
## Get JPEG snapshot |
|||
|
|||
```python |
|||
with open("snap.jpg", "wb") as f: |
|||
f.write(cam.snapshot()) |
|||
``` |
|||
|
|||
## Get video/audio bitstream |
|||
|
|||
Video-only writing to file (using simple lambda): |
|||
|
|||
```python |
|||
with open("datastream.h265", "wb") as f: |
|||
cam.start_monitor(lambda frame, meta, user: f.write(frame)) |
|||
``` |
|||
|
|||
Writing datastream with additional filtering (capture first 100 frames): |
|||
|
|||
```python |
|||
class State: |
|||
def __init__(self): |
|||
self.counter = 0 |
|||
|
|||
def count(self): |
|||
return self.counter |
|||
|
|||
def inc(self): |
|||
self.counter += 1 |
|||
|
|||
with open("datastream.h265", "wb") as f: |
|||
state = State() |
|||
def receiver(frame, meta, state): |
|||
if 'frame' in meta: |
|||
f.write(frame) |
|||
state.inc() |
|||
print(state.count()) |
|||
if state.count() == 100: |
|||
cam.stop_monitor() |
|||
|
|||
cam.start_monitor(receiver, state) |
|||
``` |
|||
|
|||
## Set camera title |
|||
|
|||
```python |
|||
# Simple way to change picture title |
|||
cam.channel_title(["Backyard"]) |
|||
|
|||
# Use unicode font from host computer to compose bitmap for title |
|||
from PIL import Image, ImageDraw, ImageFont |
|||
|
|||
w_disp = 128 |
|||
h_disp = 64 |
|||
fontsize = 32 |
|||
text = "Туалет" |
|||
|
|||
imageRGB = Image.new('RGB', (w_disp, h_disp)) |
|||
draw = ImageDraw.Draw(imageRGB) |
|||
font = ImageFont.truetype("/Library/Fonts/Arial Unicode.ttf", fontsize) |
|||
w, h = draw.textsize(text, font=font) |
|||
draw.text(((w_disp - w)/2, (h_disp - h)/2), text, font=font) |
|||
image1bit = imageRGB.convert("1") |
|||
data = image1bit.tobytes() |
|||
cam.channel_bitmap(w_disp, h_disp, data) |
|||
|
|||
# Use your own logo on picture |
|||
img = Image.open('vixand.png') |
|||
width, height = img.size |
|||
data = img.convert("1").tobytes() |
|||
cam.channel_bitmap(width, height, data) |
|||
``` |
|||
|
|||
 |
|||
|
|||
```sh |
|||
# Show current temperature, velocity, GPS coordinates, etc |
|||
# Use the same method to draw text to bitmap and transmit it to camera |
|||
# but consider place internal bitmap storage to RAM: |
|||
mount -t tmpfs -o size=100k tmpfs /mnt/mtd/tmpfs |
|||
ln -sf /mnt/mtd/tmpfs/0.dot /mnt/mtd/Config/Dot/0.dot |
|||
``` |
|||
|
|||
## OSD special text displaying |
|||
|
|||
```python |
|||
cam.set_info("fVideo.OSDInfo", {"Align": 2, "OSDInfo": [ |
|||
{ |
|||
"Info": [ |
|||
"АБВГДЕЁЖЗИКЛМНОПРСТУФХЦЧШЩЭЮЯ", |
|||
"абвгдеёжзиклмеопрстуфхцчшщэюя", |
|||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ", |
|||
"abcdefghijklmnopqrstuvwxyz", |
|||
"«»©°\"'()[]{}$%^&*_+=0123456789" |
|||
], |
|||
"OSDInfoWidget": { |
|||
"BackColor": "0x00000000", |
|||
"EncodeBlend": True, |
|||
"FrontColor": "0xD000FF00", |
|||
"PreviewBlend": True, |
|||
"RelativePos": [20, 50, 0, 0] |
|||
} |
|||
} |
|||
], "strEnc": "UTF-8"}) |
|||
``` |
|||
|
|||
 |
|||
|
|||
## Upgrade camera firmware |
|||
|
|||
```python |
|||
# Optional: get information about upgrade parameters |
|||
print(cam.get_upgrade_info()) |
|||
|
|||
# Do upgrade |
|||
cam.upgrade("General_HZXM_IPC_HI3516CV300_50H20L_AE_S38_V4.03.R12.Nat.OnvifS.HIK.20181126_ALL.bin") |
|||
``` |
|||
|
|||
## Monitor Script |
|||
|
|||
This script will persistently attempt to connect to camera at `CAMERA_IP`, will create a directory named `CAMERA_NAME` in `FILE_PATH` and start writing separate video and audio streams in files chunked in 10-minute clips, arranged in folders structured as `%Y/%m/%d`. It will also log what it does. |
|||
|
|||
```sh |
|||
./monitor.py <CAMERA_IP> <CAMERA_NAME> <FILE_PATH> |
|||
``` |
|||
|
|||
## OPFeederFunctions |
|||
|
|||
These functions are to handle the pet food dispenser when available. |
|||
You can see it with : |
|||
|
|||
```python |
|||
>>> cam.get_system_capabilities()['OtherFunction']['SupportFeederFunction'] |
|||
True |
|||
``` |
|||
|
|||
<details> |
|||
<summary>OPFeedManual</summary> |
|||
|
|||
```python |
|||
>>> cam.set_command("OPFeedManual", {"Servings": 1}) |
|||
{'Name': 'OPFeedManual', 'OPFeedManual': {'Feeded': 1, 'NotFeeding': 0}, 'Ret': 100, 'SessionID': '0x38'} |
|||
``` |
|||
|
|||
Servings is the number of portions |
|||
|
|||
</details> |
|||
|
|||
<details> |
|||
<summary>OPFeedBook</summary> |
|||
|
|||
```python |
|||
>>> cam.get_command("OPFeedBook") |
|||
{'FeedBook': [{'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '03:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '09:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '06:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '15:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '12:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '21:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '18:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 1, 'Time': '00:00:00'}, {'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 5, 'Time': '01:00:00'}]} |
|||
``` |
|||
|
|||
```python |
|||
>>> cam.set_command("OPFeedBook", {"Action": "Delete", "FeedBook": [{'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 5, 'Time': '01:00:00'}]}) |
|||
{'Name': 'OPFeedBook', 'Ret': 100, 'SessionID': '0x00000018'} |
|||
``` |
|||
|
|||
```python |
|||
>>> cam.set_command("OPFeedBook", {"Action": "Add", "FeedBook": [{'Enable': 1, 'RecDate': '2018-04-01', 'RecTime': '12:19:18', 'Servings': 5, 'Time': '01:00:00'}]}) |
|||
{'Name': 'OPFeedBook', 'Ret': 100, 'SessionID': '0x00000018'} |
|||
``` |
|||
|
|||
</details> |
|||
|
|||
<details> |
|||
<summary>OPFeedHistory</summary> |
|||
|
|||
```python |
|||
>>> cam.get_command("OPFeedHistory") |
|||
{'FeedHistory': [{'Date': '2022-08-29', 'Servings': 1, 'Time': '18:49:45', 'Type': 2}, {'Date': '2022-08-26', 'Servings': 3, 'Time': '07:30:12', 'Type': 1}]} |
|||
``` |
|||
|
|||
Type 1 : automatic |
|||
|
|||
Type 2 : manual |
|||
|
|||
```python |
|||
>>> cam.set_command("OPFeedHistory", {"Action": "Delete", "FeedHistory": [{'Date': '2022-08-29', 'Servings': 1, 'Time': '19:40:01', 'Type': 2}]}) |
|||
{'Name': 'OPFeedHistory', 'Ret': 100, 'SessionID': '0x00000027'} |
|||
``` |
|||
|
|||
</details> |
|||
|
|||
## Troubleshooting |
|||
|
|||
```python |
|||
cam.debug() |
|||
# or to enable non-standard format |
|||
cam.debug('%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
|||
``` |
|||
|
|||
## Acknowledgements |
|||
|
|||
_Telnet access creds from gabonator_ |
|||
|
|||
https://gist.github.com/gabonator/74cdd6ab4f733ff047356198c781f27d |
|||
|
@ -0,0 +1,786 @@ |
|||
import os |
|||
import struct |
|||
import json |
|||
import hashlib |
|||
import asyncio |
|||
from datetime import * |
|||
from re import compile |
|||
import time |
|||
import logging |
|||
|
|||
class SomethingIsWrongWithCamera(Exception): |
|||
pass |
|||
|
|||
class DVRIPCam(object): |
|||
DATE_FORMAT = "%Y-%m-%d %H:%M:%S" |
|||
CODES = { |
|||
100: "OK", |
|||
101: "Unknown error", |
|||
102: "Unsupported version", |
|||
103: "Request not permitted", |
|||
104: "User already logged in", |
|||
105: "User is not logged in", |
|||
106: "Username or password is incorrect", |
|||
107: "User does not have necessary permissions", |
|||
203: "Password is incorrect", |
|||
511: "Start of upgrade", |
|||
512: "Upgrade was not started", |
|||
513: "Upgrade data errors", |
|||
514: "Upgrade error", |
|||
515: "Upgrade successful", |
|||
} |
|||
QCODES = { |
|||
"AuthorityList": 1470, |
|||
"Users": 1472, |
|||
"Groups": 1474, |
|||
"AddGroup": 1476, |
|||
"ModifyGroup": 1478, |
|||
"DelGroup": 1480, |
|||
"AddUser": 1482, |
|||
"ModifyUser": 1484, |
|||
"DelUser": 1486, |
|||
"ModifyPassword": 1488, |
|||
"AlarmInfo": 1504, |
|||
"AlarmSet": 1500, |
|||
"ChannelTitle": 1046, |
|||
"EncodeCapability": 1360, |
|||
"General": 1042, |
|||
"KeepAlive": 1006, |
|||
"OPMachine": 1450, |
|||
"OPMailTest": 1636, |
|||
"OPMonitor": 1413, |
|||
"OPNetKeyboard": 1550, |
|||
"OPPTZControl": 1400, |
|||
"OPSNAP": 1560, |
|||
"OPSendFile": 0x5F2, |
|||
"OPSystemUpgrade": 0x5F5, |
|||
"OPTalk": 1434, |
|||
"OPTimeQuery": 1452, |
|||
"OPTimeSetting": 1450, |
|||
"NetWork.NetCommon": 1042, |
|||
"OPNetAlarm": 1506, |
|||
"SystemFunction": 1360, |
|||
"SystemInfo": 1020, |
|||
} |
|||
KEY_CODES = { |
|||
"M": "Menu", |
|||
"I": "Info", |
|||
"E": "Esc", |
|||
"F": "Func", |
|||
"S": "Shift", |
|||
"L": "Left", |
|||
"U": "Up", |
|||
"R": "Right", |
|||
"D": "Down", |
|||
} |
|||
OK_CODES = [100, 515] |
|||
PORTS = { |
|||
"tcp": 34567, |
|||
"udp": 34568, |
|||
} |
|||
|
|||
def __init__(self, ip, **kwargs): |
|||
self.logger = logging.getLogger(__name__) |
|||
self.ip = ip |
|||
self.user = kwargs.get("user", "admin") |
|||
self.hash_pass = kwargs.get("hash_pass", self.sofia_hash(kwargs.get("password", ""))) |
|||
self.proto = kwargs.get("proto", "tcp") |
|||
self.port = kwargs.get("port", self.PORTS.get(self.proto)) |
|||
self.socket_reader = None |
|||
self.socket_writer = None |
|||
self.packet_count = 0 |
|||
self.session = 0 |
|||
self.alive_time = 20 |
|||
self.alarm_func = None |
|||
self.timeout = 10 |
|||
self.busy = asyncio.Lock() |
|||
|
|||
def debug(self, format=None): |
|||
self.logger.setLevel(logging.DEBUG) |
|||
ch = logging.StreamHandler() |
|||
if format: |
|||
formatter = logging.Formatter(format) |
|||
ch.setFormatter(formatter) |
|||
self.logger.addHandler(ch) |
|||
|
|||
async def connect(self, timeout=10): |
|||
try: |
|||
if self.proto == "tcp": |
|||
self.socket_reader, self.socket_writer = await asyncio.wait_for(asyncio.open_connection(self.ip, self.port), timeout=timeout) |
|||
self.socket_send = self.tcp_socket_send |
|||
self.socket_recv = self.tcp_socket_recv |
|||
elif self.proto == "udp": |
|||
raise f"Unsupported protocol {self.proto} (yet)" |
|||
else: |
|||
raise f"Unsupported protocol {self.proto}" |
|||
|
|||
# it's important to extend timeout for upgrade procedure |
|||
self.timeout = timeout |
|||
except OSError: |
|||
raise SomethingIsWrongWithCamera('Cannot connect to camera') |
|||
|
|||
def close(self): |
|||
try: |
|||
self.socket_writer.close() |
|||
except: |
|||
pass |
|||
self.socket_writer = None |
|||
|
|||
def tcp_socket_send(self, bytes): |
|||
try: |
|||
return self.socket_writer.write(bytes) |
|||
except: |
|||
return None |
|||
|
|||
async def tcp_socket_recv(self, bufsize): |
|||
try: |
|||
return await self.socket_reader.read(bufsize) |
|||
except: |
|||
return None |
|||
|
|||
async def receive_with_timeout(self, length): |
|||
received = 0 |
|||
buf = bytearray() |
|||
start_time = time.time() |
|||
|
|||
while True: |
|||
try: |
|||
data = await asyncio.wait_for(self.socket_recv(length - received), timeout=self.timeout) |
|||
buf.extend(data) |
|||
received += len(data) |
|||
if length == received: |
|||
break |
|||
elapsed_time = time.time() - start_time |
|||
if elapsed_time > self.timeout: |
|||
return None |
|||
except asyncio.TimeoutError: |
|||
return None |
|||
return buf |
|||
|
|||
async def receive_json(self, length): |
|||
data = await self.receive_with_timeout(length) |
|||
if data is None: |
|||
return {} |
|||
|
|||
self.packet_count += 1 |
|||
self.logger.debug("<= %s", data) |
|||
reply = json.loads(data[:-2]) |
|||
return reply |
|||
|
|||
async def send(self, msg, data={}, wait_response=True): |
|||
if self.socket_writer is None: |
|||
return {"Ret": 101} |
|||
await self.busy.acquire() |
|||
if hasattr(data, "__iter__"): |
|||
data = bytes(json.dumps(data, ensure_ascii=False), "utf-8") |
|||
pkt = ( |
|||
struct.pack( |
|||
"BB2xII2xHI", |
|||
255, |
|||
0, |
|||
self.session, |
|||
self.packet_count, |
|||
msg, |
|||
len(data) + 2, |
|||
) |
|||
+ data |
|||
+ b"\x0a\x00" |
|||
) |
|||
self.logger.debug("=> %s", pkt) |
|||
self.socket_send(pkt) |
|||
if wait_response: |
|||
reply = {"Ret": 101} |
|||
data = await self.socket_recv(20) |
|||
if data is None or len(data) < 20: |
|||
return None |
|||
( |
|||
head, |
|||
version, |
|||
self.session, |
|||
sequence_number, |
|||
msgid, |
|||
len_data, |
|||
) = struct.unpack("BB2xII2xHI", data) |
|||
reply = await self.receive_json(len_data) |
|||
self.busy.release() |
|||
return reply |
|||
|
|||
def sofia_hash(self, password=""): |
|||
md5 = hashlib.md5(bytes(password, "utf-8")).digest() |
|||
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" |
|||
return "".join([chars[sum(x) % 62] for x in zip(md5[::2], md5[1::2])]) |
|||
|
|||
async def login(self, loop): |
|||
if self.socket_writer is None: |
|||
await self.connect() |
|||
data = await self.send( |
|||
1000, |
|||
{ |
|||
"EncryptType": "MD5", |
|||
"LoginType": "DVRIP-Web", |
|||
"PassWord": self.hash_pass, |
|||
"UserName": self.user, |
|||
}, |
|||
) |
|||
if data is None or data["Ret"] not in self.OK_CODES: |
|||
return False |
|||
self.session = int(data["SessionID"], 16) |
|||
self.alive_time = data["AliveInterval"] |
|||
self.keep_alive(loop) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def getAuthorityList(self): |
|||
data = await self.send(self.QCODES["AuthorityList"]) |
|||
if data["Ret"] in self.OK_CODES: |
|||
return data["AuthorityList"] |
|||
else: |
|||
return [] |
|||
|
|||
async def getGroups(self): |
|||
data = await self.send(self.QCODES["Groups"]) |
|||
if data["Ret"] in self.OK_CODES: |
|||
return data["Groups"] |
|||
else: |
|||
return [] |
|||
|
|||
async def addGroup(self, name, comment="", auth=None): |
|||
data = await self.set_command( |
|||
"AddGroup", |
|||
{ |
|||
"Group": { |
|||
"AuthorityList": auth or await self.getAuthorityList(), |
|||
"Memo": comment, |
|||
"Name": name, |
|||
}, |
|||
}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def modifyGroup(self, name, newname=None, comment=None, auth=None): |
|||
g = [x for x in await self.getGroups() if x["Name"] == name] |
|||
if g == []: |
|||
print(f'Group "{name}" not found!') |
|||
return False |
|||
g = g[0] |
|||
data = await self.send( |
|||
self.QCODES["ModifyGroup"], |
|||
{ |
|||
"Group": { |
|||
"AuthorityList": auth or g["AuthorityList"], |
|||
"Memo": comment or g["Memo"], |
|||
"Name": newname or g["Name"], |
|||
}, |
|||
"GroupName": name, |
|||
}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def delGroup(self, name): |
|||
data = await self.send( |
|||
self.QCODES["DelGroup"], |
|||
{"Name": name, "SessionID": "0x%08X" % self.session,}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def getUsers(self): |
|||
data = await self.send(self.QCODES["Users"]) |
|||
if data["Ret"] in self.OK_CODES: |
|||
return data["Users"] |
|||
else: |
|||
return [] |
|||
|
|||
async def addUser( |
|||
self, name, password, comment="", group="user", auth=None, sharable=True |
|||
): |
|||
g = [x for x in await self.getGroups() if x["Name"] == group] |
|||
if g == []: |
|||
print(f'Group "{group}" not found!') |
|||
return False |
|||
g = g[0] |
|||
data = await self.set_command( |
|||
"AddUser", |
|||
{ |
|||
"User": { |
|||
"AuthorityList": auth or g["AuthorityList"], |
|||
"Group": g["Name"], |
|||
"Memo": comment, |
|||
"Name": name, |
|||
"Password": self.sofia_hash(password), |
|||
"Reserved": False, |
|||
"Sharable": sharable, |
|||
}, |
|||
}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def modifyUser( |
|||
self, name, newname=None, comment=None, group=None, auth=None, sharable=None |
|||
): |
|||
u = [x for x in self.getUsers() if x["Name"] == name] |
|||
if u == []: |
|||
print(f'User "{name}" not found!') |
|||
return False |
|||
u = u[0] |
|||
if group: |
|||
g = [x for x in await self.getGroups() if x["Name"] == group] |
|||
if g == []: |
|||
print(f'Group "{group}" not found!') |
|||
return False |
|||
u["AuthorityList"] = g[0]["AuthorityList"] |
|||
data = await self.send( |
|||
self.QCODES["ModifyUser"], |
|||
{ |
|||
"User": { |
|||
"AuthorityList": auth or u["AuthorityList"], |
|||
"Group": group or u["Group"], |
|||
"Memo": comment or u["Memo"], |
|||
"Name": newname or u["Name"], |
|||
"Password": "", |
|||
"Reserved": u["Reserved"], |
|||
"Sharable": sharable or u["Sharable"], |
|||
}, |
|||
"UserName": name, |
|||
}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def delUser(self, name): |
|||
data = await self.send( |
|||
self.QCODES["DelUser"], |
|||
{"Name": name, "SessionID": "0x%08X" % self.session,}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def changePasswd(self, newpass="", oldpass=None, user=None): |
|||
data = await self.send( |
|||
self.QCODES["ModifyPassword"], |
|||
{ |
|||
"EncryptType": "MD5", |
|||
"NewPassWord": self.sofia_hash(newpass), |
|||
"PassWord": oldpass or self.password, |
|||
"SessionID": "0x%08X" % self.session, |
|||
"UserName": user or self.user, |
|||
}, |
|||
) |
|||
return data["Ret"] in self.OK_CODES |
|||
|
|||
async def channel_title(self, titles): |
|||
if isinstance(titles, str): |
|||
titles = [titles] |
|||
await self.send( |
|||
self.QCODES["ChannelTitle"], |
|||
{ |
|||
"ChannelTitle": titles, |
|||
"Name": "ChannelTitle", |
|||
"SessionID": "0x%08X" % self.session, |
|||
}, |
|||
) |
|||
|
|||
async def channel_bitmap(self, width, height, bitmap): |
|||
header = struct.pack("HH12x", width, height) |
|||
self.socket_send( |
|||
struct.pack( |
|||
"BB2xII2xHI", |
|||
255, |
|||
0, |
|||
self.session, |
|||
self.packet_count, |
|||
0x041A, |
|||
len(bitmap) + 16, |
|||
) |
|||
+ header |
|||
+ bitmap |
|||
) |
|||
reply, rcvd = await self.recv_json() |
|||
if reply and reply["Ret"] != 100: |
|||
return False |
|||
return True |
|||
|
|||
async def reboot(self): |
|||
await self.set_command("OPMachine", {"Action": "Reboot"}) |
|||
self.close() |
|||
|
|||
def setAlarm(self, func): |
|||
self.alarm_func = func |
|||
|
|||
def clearAlarm(self): |
|||
self.alarm_func = None |
|||
|
|||
async def alarmStart(self, loop): |
|||
loop.create_task(self.alarm_worker()) |
|||
|
|||
return await self.get_command("", self.QCODES["AlarmSet"]) |
|||
|
|||
async def alarm_worker(self): |
|||
while self.socket_writer: |
|||
await self.busy.acquire() |
|||
try: |
|||
( |
|||
head, |
|||
version, |
|||
session, |
|||
sequence_number, |
|||
msgid, |
|||
len_data, |
|||
) = struct.unpack("BB2xII2xHI", await self.socket_recv(20)) |
|||
await asyncio.sleep(0.1) # Just for receive whole packet |
|||
reply = await self.socket_recv(len_data) |
|||
self.packet_count += 1 |
|||
reply = json.loads(reply[:-2]) |
|||
if msgid == self.QCODES["AlarmInfo"] and self.session == session: |
|||
if self.alarm_func is not None: |
|||
self.alarm_func(reply[reply["Name"]], sequence_number) |
|||
except: |
|||
pass |
|||
finally: |
|||
self.busy.release() |
|||
|
|||
async def set_remote_alarm(self, state): |
|||
await self.set_command( |
|||
"OPNetAlarm", {"Event": 0, "State": state}, |
|||
) |
|||
|
|||
async def keep_alive_workner(self): |
|||
while self.socket_writer: |
|||
|
|||
ret = await self.send( |
|||
self.QCODES["KeepAlive"], |
|||
{"Name": "KeepAlive", "SessionID": "0x%08X" % self.session}, |
|||
) |
|||
if ret is None: |
|||
self.close() |
|||
break |
|||
|
|||
await asyncio.sleep(self.alive_time) |
|||
|
|||
def keep_alive(self, loop): |
|||
loop.create_task(self.keep_alive_workner()) |
|||
|
|||
async def keyDown(self, key): |
|||
await self.set_command( |
|||
"OPNetKeyboard", {"Status": "KeyDown", "Value": key}, |
|||
) |
|||
|
|||
async def keyUp(self, key): |
|||
await self.set_command( |
|||
"OPNetKeyboard", {"Status": "KeyUp", "Value": key}, |
|||
) |
|||
|
|||
async def keyPress(self, key): |
|||
await self.keyDown(key) |
|||
await asyncio.sleep(0.3) |
|||
await self.keyUp(key) |
|||
|
|||
async def keyScript(self, keys): |
|||
for k in keys: |
|||
if k != " " and k.upper() in self.KEY_CODES: |
|||
await self.keyPress(self.KEY_CODES[k.upper()]) |
|||
else: |
|||
await asyncio.sleep(1) |
|||
|
|||
async def ptz(self, cmd, step=5, preset=-1, ch=0): |
|||
CMDS = [ |
|||
"DirectionUp", |
|||
"DirectionDown", |
|||
"DirectionLeft", |
|||
"DirectionRight", |
|||
"DirectionLeftUp", |
|||
"DirectionLeftDown", |
|||
"DirectionRightUp", |
|||
"DirectionRightDown", |
|||
"ZoomTile", |
|||
"ZoomWide", |
|||
"FocusNear", |
|||
"FocusFar", |
|||
"IrisSmall", |
|||
"IrisLarge", |
|||
"SetPreset", |
|||
"GotoPreset", |
|||
"ClearPreset", |
|||
"StartTour", |
|||
"StopTour", |
|||
] |
|||
# ptz_param = { "AUX" : { "Number" : 0, "Status" : "On" }, "Channel" : ch, "MenuOpts" : "Enter", "POINT" : { "bottom" : 0, "left" : 0, "right" : 0, "top" : 0 }, "Pattern" : "SetBegin", "Preset" : -1, "Step" : 5, "Tour" : 0 } |
|||
ptz_param = { |
|||
"AUX": {"Number": 0, "Status": "On"}, |
|||
"Channel": ch, |
|||
"MenuOpts": "Enter", |
|||
"Pattern": "Start", |
|||
"Preset": preset, |
|||
"Step": step, |
|||
"Tour": 1 if "Tour" in cmd else 0, |
|||
} |
|||
return await self.set_command( |
|||
"OPPTZControl", {"Command": cmd, "Parameter": ptz_param}, |
|||
) |
|||
|
|||
async def set_info(self, command, data): |
|||
return await self.set_command(command, data, 1040) |
|||
|
|||
async def set_command(self, command, data, code=None): |
|||
if not code: |
|||
code = self.QCODES[command] |
|||
return await self.send( |
|||
code, {"Name": command, "SessionID": "0x%08X" % self.session, command: data} |
|||
) |
|||
|
|||
async def get_info(self, command): |
|||
return await self.get_command(command, 1042) |
|||
|
|||
async def get_command(self, command, code=None): |
|||
if not code: |
|||
code = self.QCODES[command] |
|||
|
|||
data = await self.send(code, {"Name": command, "SessionID": "0x%08X" % self.session}) |
|||
if data["Ret"] in self.OK_CODES and command in data: |
|||
return data[command] |
|||
else: |
|||
return data |
|||
|
|||
async def get_time(self): |
|||
return datetime.strptime(await self.get_command("OPTimeQuery"), self.DATE_FORMAT) |
|||
|
|||
async def set_time(self, time=None): |
|||
if time is None: |
|||
time = datetime.now() |
|||
return await self.set_command("OPTimeSetting", time.strftime(self.DATE_FORMAT)) |
|||
|
|||
async def get_netcommon(self): |
|||
return await self.get_command("NetWork.NetCommon") |
|||
|
|||
async def get_system_info(self): |
|||
return await self.get_command("SystemInfo") |
|||
|
|||
async def get_general_info(self): |
|||
return await self.get_command("General") |
|||
|
|||
async def get_encode_capabilities(self): |
|||
return await self.get_command("EncodeCapability") |
|||
|
|||
async def get_system_capabilities(self): |
|||
return await self.get_command("SystemFunction") |
|||
|
|||
async def get_camera_info(self, default_config=False): |
|||
"""Request data for 'Camera' from the target DVRIP device.""" |
|||
if default_config: |
|||
code = 1044 |
|||
else: |
|||
code = 1042 |
|||
return await self.get_command("Camera", code) |
|||
|
|||
async def get_encode_info(self, default_config=False): |
|||
"""Request data for 'Simplify.Encode' from the target DVRIP device. |
|||
|
|||
Arguments: |
|||
default_config -- returns the default values for the type if True |
|||
""" |
|||
if default_config: |
|||
code = 1044 |
|||
else: |
|||
code = 1042 |
|||
return await self.get_command("Simplify.Encode", code) |
|||
|
|||
async def recv_json(self, buf=bytearray()): |
|||
p = compile(b".*({.*})") |
|||
|
|||
packet = await self.socket_recv(0xFFFF) |
|||
if not packet: |
|||
return None, buf |
|||
buf.extend(packet) |
|||
m = p.search(buf) |
|||
if m is None: |
|||
return None, buf |
|||
buf = buf[m.span(1)[1] :] |
|||
return json.loads(m.group(1)), buf |
|||
|
|||
async def get_upgrade_info(self): |
|||
return await self.get_command("OPSystemUpgrade") |
|||
|
|||
async def upgrade(self, filename="", packetsize=0x8000, vprint=None): |
|||
if not vprint: |
|||
vprint = lambda x: print(x) |
|||
|
|||
data = await self.set_command( |
|||
"OPSystemUpgrade", {"Action": "Start", "Type": "System"}, 0x5F0 |
|||
) |
|||
if data["Ret"] not in self.OK_CODES: |
|||
return data |
|||
|
|||
vprint("Ready to upgrade") |
|||
blocknum = 0 |
|||
sentbytes = 0 |
|||
fsize = os.stat(filename).st_size |
|||
rcvd = bytearray() |
|||
with open(filename, "rb") as f: |
|||
while True: |
|||
bytes = f.read(packetsize) |
|||
if not bytes: |
|||
break |
|||
header = struct.pack( |
|||
"BB2xII2xHI", 255, 0, self.session, blocknum, 0x5F2, len(bytes) |
|||
) |
|||
self.socket_send(header + bytes) |
|||
blocknum += 1 |
|||
sentbytes += len(bytes) |
|||
|
|||
reply, rcvd = await self.recv_json(rcvd) |
|||
if reply and reply["Ret"] != 100: |
|||
vprint("Upgrade failed") |
|||
return reply |
|||
|
|||
progress = sentbytes / fsize * 100 |
|||
vprint(f"Uploaded {progress:.2f}%") |
|||
vprint("End of file") |
|||
|
|||
pkt = struct.pack("BB2xIIxBHI", 255, 0, self.session, blocknum, 1, 0x05F2, 0) |
|||
self.socket_send(pkt) |
|||
vprint("Waiting for upgrade...") |
|||
while True: |
|||
reply, rcvd = await self.recv_json(rcvd) |
|||
print(reply) |
|||
if not reply: |
|||
return |
|||
if reply["Name"] == "" and reply["Ret"] == 100: |
|||
break |
|||
|
|||
while True: |
|||
data, rcvd = await self.recv_json(rcvd) |
|||
print(reply) |
|||
if data is None: |
|||
vprint("Done") |
|||
return |
|||
if data["Ret"] in [512, 514, 513]: |
|||
vprint("Upgrade failed") |
|||
return data |
|||
if data["Ret"] == 515: |
|||
vprint("Upgrade successful") |
|||
self.close() |
|||
return data |
|||
vprint(f"Upgraded {data['Ret']}%") |
|||
|
|||
async def reassemble_bin_payload(self, metadata={}): |
|||
def internal_to_type(data_type, value): |
|||
if data_type == 0x1FC or data_type == 0x1FD: |
|||
if value == 1: |
|||
return "mpeg4" |
|||
elif value == 2: |
|||
return "h264" |
|||
elif value == 3: |
|||
return "h265" |
|||
elif data_type == 0x1F9: |
|||
if value == 1 or value == 6: |
|||
return "info" |
|||
elif data_type == 0x1FA: |
|||
if value == 0xE: |
|||
return "g711a" |
|||
elif data_type == 0x1FE and value == 0: |
|||
return "jpeg" |
|||
return None |
|||
|
|||
def internal_to_datetime(value): |
|||
second = value & 0x3F |
|||
minute = (value & 0xFC0) >> 6 |
|||
hour = (value & 0x1F000) >> 12 |
|||
day = (value & 0x3E0000) >> 17 |
|||
month = (value & 0x3C00000) >> 22 |
|||
year = ((value & 0xFC000000) >> 26) + 2000 |
|||
return datetime(year, month, day, hour, minute, second) |
|||
|
|||
length = 0 |
|||
buf = bytearray() |
|||
start_time = time.time() |
|||
|
|||
while True: |
|||
data = await self.receive_with_timeout(20) |
|||
( |
|||
head, |
|||
version, |
|||
session, |
|||
sequence_number, |
|||
total, |
|||
cur, |
|||
msgid, |
|||
len_data, |
|||
) = struct.unpack("BB2xIIBBHI", data) |
|||
packet = await self.receive_with_timeout(len_data) |
|||
frame_len = 0 |
|||
if length == 0: |
|||
media = None |
|||
frame_len = 8 |
|||
(data_type,) = struct.unpack(">I", packet[:4]) |
|||
if data_type == 0x1FC or data_type == 0x1FE: |
|||
frame_len = 16 |
|||
(media, metadata["fps"], w, h, dt, length,) = struct.unpack( |
|||
"BBBBII", packet[4:frame_len] |
|||
) |
|||
metadata["width"] = w * 8 |
|||
metadata["height"] = h * 8 |
|||
metadata["datetime"] = internal_to_datetime(dt) |
|||
if data_type == 0x1FC: |
|||
metadata["frame"] = "I" |
|||
elif data_type == 0x1FD: |
|||
(length,) = struct.unpack("I", packet[4:frame_len]) |
|||
metadata["frame"] = "P" |
|||
elif data_type == 0x1FA: |
|||
(media, samp_rate, length) = struct.unpack( |
|||
"BBH", packet[4:frame_len] |
|||
) |
|||
elif data_type == 0x1F9: |
|||
(media, n, length) = struct.unpack("BBH", packet[4:frame_len]) |
|||
# special case of JPEG shapshots |
|||
elif data_type == 0xFFD8FFE0: |
|||
return packet |
|||
else: |
|||
raise ValueError(data_type) |
|||
if media is not None: |
|||
metadata["type"] = internal_to_type(data_type, media) |
|||
buf.extend(packet[frame_len:]) |
|||
length -= len(packet) - frame_len |
|||
if length == 0: |
|||
return buf |
|||
elapsed_time = time.time() - start_time |
|||
if elapsed_time > self.timeout: |
|||
return None |
|||
|
|||
async def snapshot(self, channel=0): |
|||
command = "OPSNAP" |
|||
await self.send( |
|||
self.QCODES[command], |
|||
{ |
|||
"Name": command, |
|||
"SessionID": "0x%08X" % self.session, |
|||
command: {"Channel": channel}, |
|||
}, |
|||
wait_response=False, |
|||
) |
|||
packet = await self.reassemble_bin_payload() |
|||
return packet |
|||
|
|||
async def start_monitor(self, frame_callback, user={}, stream="Main"): |
|||
params = { |
|||
"Channel": 0, |
|||
"CombinMode": "NONE", |
|||
"StreamType": stream, |
|||
"TransMode": "TCP", |
|||
} |
|||
data = await self.set_command("OPMonitor", {"Action": "Claim", "Parameter": params}) |
|||
if data["Ret"] not in self.OK_CODES: |
|||
return data |
|||
|
|||
await self.send( |
|||
1410, |
|||
{ |
|||
"Name": "OPMonitor", |
|||
"SessionID": "0x%08X" % self.session, |
|||
"OPMonitor": {"Action": "Start", "Parameter": params}, |
|||
}, |
|||
wait_response=False, |
|||
) |
|||
self.monitoring = True |
|||
while self.monitoring: |
|||
meta = {} |
|||
frame = await self.reassemble_bin_payload(meta) |
|||
frame_callback(frame, meta, user) |
|||
|
|||
def stop_monitor(self): |
|||
self.monitoring = False |
@ -0,0 +1,46 @@ |
|||
#!/usr/bin/env python |
|||
# -*- coding: UTF-8 -*- |
|||
import sys |
|||
from dvrip import DVRIPCam |
|||
from time import sleep |
|||
import json |
|||
|
|||
host_ip = "192.168.0.100" |
|||
if len(sys.argv) > 1: |
|||
host_ip = str(sys.argv[1]) |
|||
|
|||
cam = DVRIPCam(host_ip, user="admin", password="46216") |
|||
|
|||
if cam.login(): |
|||
print("Success! Connected to " + host_ip) |
|||
else: |
|||
print("Failure. Could not connect.") |
|||
|
|||
info = cam.get_info("fVideo.OSDInfo") |
|||
print(json.dumps(info, ensure_ascii=False)) |
|||
info["OSDInfo"][0]["Info"] = [u"Тест00", "Test01", "Test02"] |
|||
# info["OSDInfo"][0]["Info"][1] = "" |
|||
# info["OSDInfo"][0]["Info"][2] = "" |
|||
# info["OSDInfo"][0]["Info"][3] = "Test3" |
|||
info["OSDInfo"][0]["OSDInfoWidget"]["EncodeBlend"] = True |
|||
info["OSDInfo"][0]["OSDInfoWidget"]["PreviewBlend"] = True |
|||
# info["OSDInfo"][0]["OSDInfoWidget"]["RelativePos"] = [6144,6144,8192,8192] |
|||
cam.set_info("fVideo.OSDInfo", info) |
|||
# enc_info = cam.get_info("Simplify.Encode") |
|||
# Alarm example |
|||
def alarm(content, ids): |
|||
print(content) |
|||
|
|||
|
|||
cam.setAlarm(alarm) |
|||
cam.alarmStart() |
|||
# cam.get_encode_info() |
|||
# sleep(1) |
|||
# cam.get_camera_info() |
|||
# sleep(1) |
|||
|
|||
# enc_info[0]['ExtraFormat']['Video']['FPS'] = 20 |
|||
# cam.set_info("Simplify.Encode", enc_info) |
|||
# sleep(2) |
|||
# print(cam.get_info("Simplify.Encode")) |
|||
# cam.close() |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,127 @@ |
|||
from pathlib import Path |
|||
from time import sleep |
|||
import os |
|||
import json |
|||
import logging |
|||
from collections import namedtuple |
|||
from solarcam import SolarCam |
|||
|
|||
|
|||
def init_logger(): |
|||
logger = logging.getLogger(__name__) |
|||
logger.setLevel(logging.DEBUG) |
|||
ch = logging.StreamHandler() |
|||
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") |
|||
ch.setFormatter(formatter) |
|||
logger.addHandler(ch) |
|||
return logger |
|||
|
|||
|
|||
def load_config(): |
|||
def config_decoder(config_dict): |
|||
return namedtuple("X", config_dict.keys())(*config_dict.values()) |
|||
|
|||
config_path = os.environ.get("CONFIG_PATH") |
|||
if Path(config_path).exists(): |
|||
with open(config_path, "r") as file: |
|||
return json.loads(file.read(), object_hook=config_decoder) |
|||
|
|||
return { |
|||
"host_ip": os.environ.get("IP_ADDRESS"), |
|||
"user": os.environ.get("USER"), |
|||
"password": os.environ.get("PASSWORD"), |
|||
"target_filetype_video": os.environ.get("target_filetype_video"), |
|||
"download_dir_video": os.environ.get("DOWNLOAD_DIR_VIDEO"), |
|||
"download_dir_picture": os.environ.get("DOWNLOAD_DIR_PICTURE"), |
|||
"start": os.environ.get("START"), |
|||
"end": os.environ.get("END"), |
|||
"blacklist_path": os.environ.get("BLACKLIST_PATH"), |
|||
"cooldown": int(os.environ.get("COOLDOWN")), |
|||
"dump_local_files": ( |
|||
os.environ.get("DUMP_LOCAL_FILES").lower() in ["true", "1", "y", "yes"] |
|||
), |
|||
} |
|||
|
|||
|
|||
def main(): |
|||
logger = init_logger() |
|||
config = load_config() |
|||
start = config.start |
|||
end = config.end |
|||
cooldown = config.cooldown |
|||
|
|||
blacklist = None |
|||
if Path(config.blacklist_path).exists(): |
|||
with open(config.blacklist_path, "r") as file: |
|||
blacklist = [line.rstrip() for line in file] |
|||
|
|||
while True: |
|||
solarCam = SolarCam(config.host_ip, config.user, config.password, logger) |
|||
|
|||
try: |
|||
solarCam.login() |
|||
|
|||
battery = solarCam.get_battery() |
|||
logger.debug(f"Current battery status: {battery}") |
|||
storage = solarCam.get_storage()[0] |
|||
logger.debug(f"Current storage status: {storage}") |
|||
|
|||
logger.debug(f"Syncing time...") |
|||
solarCam.set_time() # setting it to system clock |
|||
logger.debug(f"Camera time is now {solarCam.get_time()}") |
|||
|
|||
sleep(5) # sleep some seconds so camera can get ready |
|||
|
|||
pics = solarCam.get_local_files(start, end, "jpg") |
|||
|
|||
if pics: |
|||
Path(config.download_dir_picture).parent.mkdir( |
|||
parents=True, exist_ok=True |
|||
) |
|||
solarCam.save_files( |
|||
config.download_dir_picture, pics, blacklist=blacklist |
|||
) |
|||
|
|||
videos = solarCam.get_local_files(start, end, "h264") |
|||
if videos: |
|||
Path(config.download_dir_video).parent.mkdir( |
|||
parents=True, exist_ok=True |
|||
) |
|||
solarCam.save_files( |
|||
config.download_dir_video, |
|||
videos, |
|||
blacklist=blacklist, |
|||
target_filetype=config.target_filetype_video, |
|||
) |
|||
|
|||
if config.dump_local_files: |
|||
logger.debug(f"Dumping local files...") |
|||
solarCam.dump_local_files( |
|||
videos, |
|||
config.blacklist_path, |
|||
config.download_dir_video, |
|||
target_filetype=config.target_filetype_video, |
|||
) |
|||
solarCam.dump_local_files( |
|||
pics, config.blacklist_path, config.download_dir_picture |
|||
) |
|||
|
|||
solarCam.logout() |
|||
except ConnectionRefusedError: |
|||
logger.debug(f"Connection could not be established or got disconnected") |
|||
except TypeError as e: |
|||
print(e) |
|||
logger.debug(f"Error while downloading a file") |
|||
except KeyError: |
|||
logger.debug(f"Error while getting the file list") |
|||
logger.debug(f"Sleeping for {cooldown} seconds...") |
|||
sleep(cooldown) |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
|
|||
# todo add flask api for moving cam |
|||
# todo show current stream |
|||
# todo show battery on webinterface and write it to mqtt topic |
|||
# todo change camera name |
File diff suppressed because it is too large
@ -0,0 +1,17 @@ |
|||
FROM python:3.10-slim-buster |
|||
|
|||
RUN apt-get update && \ |
|||
apt-get upgrade -y && \ |
|||
apt-get install -y \ |
|||
git \ |
|||
curl |
|||
|
|||
WORKDIR /app |
|||
|
|||
COPY . . |
|||
|
|||
RUN pip3 install -r requirements.txt |
|||
|
|||
EXPOSE 8888 |
|||
|
|||
CMD [ "python3", "./app.py"] |
@ -0,0 +1,15 @@ |
|||
### SocketIO example |
|||
|
|||
Build image |
|||
```bash |
|||
docker build -t video-stream . |
|||
``` |
|||
|
|||
Run container |
|||
```bash |
|||
docker run -d \ |
|||
--restart always \ |
|||
--network host \ |
|||
--name video-stream \ |
|||
video-stream |
|||
``` |
@ -0,0 +1,107 @@ |
|||
import socketio |
|||
from asyncio_dvrip import DVRIPCam |
|||
from aiohttp import web |
|||
import asyncio |
|||
import signal |
|||
import traceback |
|||
import base64 |
|||
|
|||
loop = asyncio.get_event_loop() |
|||
queue = asyncio.Queue() |
|||
|
|||
# socket clients |
|||
clients = [] |
|||
sio = socketio.AsyncServer() |
|||
app = web.Application() |
|||
sio.attach(app) |
|||
|
|||
@sio.event |
|||
def connect(sid, environ): |
|||
print("connect ", sid) |
|||
clients.append(sid) |
|||
|
|||
@sio.event |
|||
def my_message(sid, data): |
|||
print('message ', data) |
|||
|
|||
@sio.event |
|||
def disconnect(sid): |
|||
print('disconnect ', sid) |
|||
clients.remove(sid) |
|||
|
|||
def stop(loop): |
|||
loop.remove_signal_handler(signal.SIGTERM) |
|||
tasks = asyncio.gather(*asyncio.Task.all_tasks(loop=loop), loop=loop, return_exceptions=True) |
|||
tasks.add_done_callback(lambda t: loop.stop()) |
|||
tasks.cancel() |
|||
|
|||
async def stream(loop, queue): |
|||
cam = DVRIPCam("192.168.0.100", port=34567, user="admin", password="") |
|||
# login |
|||
if not await cam.login(loop): |
|||
raise Exception("Can't open cam") |
|||
|
|||
try: |
|||
await cam.start_monitor(lambda frame, meta, user: queue.put_nowait(frame), stream="Main") |
|||
except Exception as err: |
|||
msg = ''.join(traceback.format_tb(err.__traceback__) + [str(err)]) |
|||
print(msg) |
|||
finally: |
|||
cam.stop_monitor() |
|||
cam.close() |
|||
|
|||
async def process(queue, lock): |
|||
while True: |
|||
frame = await queue.get() |
|||
|
|||
if frame: |
|||
await lock.acquire() |
|||
try: |
|||
for sid in clients: |
|||
await sio.emit('message', {'data': base64.b64encode(frame).decode("utf-8")}, room=sid) |
|||
finally: |
|||
lock.release() |
|||
|
|||
async def worker(loop, queue, lock): |
|||
task = None |
|||
|
|||
# infinyty loop |
|||
while True: |
|||
await lock.acquire() |
|||
|
|||
try: |
|||
# got clients and task not started |
|||
if len(clients) > 0 and task is None: |
|||
# create stream task |
|||
task = loop.create_task(stream(loop, queue)) |
|||
|
|||
# no more clients, neet stop task |
|||
if len(clients) == 0 and task is not None: |
|||
# I don't like this way, maybe someone can do it better |
|||
task.cancel() |
|||
task = None |
|||
await asyncio.sleep(0.1) |
|||
except Exception as err: |
|||
msg = ''.join(traceback.format_tb(err.__traceback__) + [str(err)]) |
|||
print(msg) |
|||
finally: |
|||
lock.release() |
|||
|
|||
if __name__ == '__main__': |
|||
try: |
|||
lock = asyncio.Lock() |
|||
|
|||
# run wb application |
|||
runner = web.AppRunner(app) |
|||
loop.run_until_complete(runner.setup()) |
|||
site = web.TCPSite(runner, host='0.0.0.0', port=8888) |
|||
loop.run_until_complete(site.start()) |
|||
|
|||
# run worker |
|||
loop.create_task(worker(loop, queue, lock)) |
|||
loop.create_task(process(queue, lock)) |
|||
|
|||
# wait stop |
|||
loop.run_forever() |
|||
except: |
|||
stop(loop) |
@ -0,0 +1,18 @@ |
|||
import socketio |
|||
|
|||
# standard Python |
|||
sio = socketio.Client() |
|||
|
|||
@sio.event |
|||
def connect(): |
|||
print("I'm connected!") |
|||
|
|||
@sio.event |
|||
def connect_error(): |
|||
print("The connection failed!") |
|||
|
|||
@sio.on('message') |
|||
def on_message(data): |
|||
print('frame', data) |
|||
|
|||
sio.connect('http://localhost:8888') |
@ -0,0 +1,14 @@ |
|||
aiohttp==3.8.5 |
|||
aiosignal==1.3.1 |
|||
async-timeout==4.0.2 |
|||
asyncio==3.4.3 |
|||
attrs==22.1.0 |
|||
bidict==0.22.0 |
|||
charset-normalizer==2.1.1 |
|||
frozenlist==1.3.3 |
|||
idna==3.4 |
|||
multidict==6.0.2 |
|||
python-dvr @ git+https://github.com/NeiroNx/python-dvr@06ff6dc0082767e7c9f23401f828533459f783a4 |
|||
python-engineio==4.3.4 |
|||
python-socketio==5.7.2 |
|||
yarl==1.8.1 |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 73 KiB |
After Width: | Height: | Size: 77 KiB |
@ -0,0 +1,121 @@ |
|||
#! /usr/bin/python3 |
|||
from dvrip import DVRIPCam, SomethingIsWrongWithCamera |
|||
from signal import signal, SIGINT, SIGTERM |
|||
from sys import argv, stdout, exit |
|||
from datetime import datetime |
|||
from pathlib import Path |
|||
from time import sleep, time |
|||
import logging |
|||
|
|||
baseDir = argv[3] |
|||
retryIn = 5 |
|||
rebootWait = 10 |
|||
camIp = argv[1] |
|||
camName = argv[2] |
|||
cam = None |
|||
isShuttingDown = False |
|||
chunkSize = 600 # new file every 10 minutes |
|||
logFile = baseDir + '/' + camName + '/log.log' |
|||
|
|||
def log(str): |
|||
logging.info(str) |
|||
|
|||
def mkpath(): |
|||
path = baseDir + '/' + camName + "/" + datetime.today().strftime('%Y/%m/%d/%H.%M.%S') |
|||
Path(path).parent.mkdir(parents=True, exist_ok=True) |
|||
return path |
|||
|
|||
def shutDown(): |
|||
global isShuttingDown |
|||
isShuttingDown = True |
|||
log('Shutting down...') |
|||
try: |
|||
cam.stop_monitor() |
|||
close() |
|||
except (RuntimeError, TypeError, NameError, Exception): |
|||
pass |
|||
log('done') |
|||
exit(0) |
|||
|
|||
def handler(signum, b): |
|||
log('Signal ' + str(signum) + ' received') |
|||
shutDown() |
|||
|
|||
signal(SIGINT, handler) |
|||
signal(SIGTERM, handler) |
|||
|
|||
def close(): |
|||
cam.close() |
|||
|
|||
def theActualJob(): |
|||
|
|||
prevtime = 0 |
|||
video = None |
|||
audio = None |
|||
|
|||
def receiver(frame, meta, user): |
|||
nonlocal prevtime, video, audio |
|||
if frame is None: |
|||
log('Empty frame') |
|||
else: |
|||
tn = time() |
|||
if tn - prevtime >= chunkSize: |
|||
if video != None: |
|||
video.close() |
|||
audio.close() |
|||
prevtime = tn |
|||
path = mkpath() |
|||
log('Starting files: ' + path) |
|||
video = open(path + '.video', "wb") |
|||
audio = open(path + '.audio', "wb") |
|||
if 'type' in meta and meta["type"] == "g711a": audio.write(frame) |
|||
elif 'frame' in meta: video.write(frame) |
|||
|
|||
log('Starting to grab streams...') |
|||
cam.start_monitor(receiver) |
|||
|
|||
def syncTime(): |
|||
log('Synching time...') |
|||
cam.set_time() |
|||
log('done') |
|||
|
|||
def jobWrapper(): |
|||
global cam |
|||
log('Logging in to camera ' + camIp + '...') |
|||
cam = DVRIPCam(camIp) |
|||
if cam.login(): |
|||
log('done') |
|||
else: |
|||
raise SomethingIsWrongWithCamera('Cannot login') |
|||
syncTime() |
|||
theActualJob() |
|||
|
|||
def theJob(): |
|||
while True: |
|||
try: |
|||
jobWrapper() |
|||
except (TypeError, ValueError) as err: |
|||
if isShuttingDown: |
|||
exit(0) |
|||
else: |
|||
try: |
|||
log('Error. Attempting to reboot camera...') |
|||
cam.reboot() |
|||
log('Waiting for ' + str(rebootWait) + 's for reboot...') |
|||
sleep(rebootWait) |
|||
except (UnicodeDecodeError, ValueError, TypeError): |
|||
raise SomethingIsWrongWithCamera('Failed to reboot') |
|||
|
|||
def main(): |
|||
Path(logFile).parent.mkdir(parents=True, exist_ok=True) |
|||
logging.basicConfig(filename=logFile, level=logging.INFO, format='[%(asctime)s] %(message)s') |
|||
while True: |
|||
try: |
|||
theJob() |
|||
except SomethingIsWrongWithCamera as err: |
|||
close() |
|||
log(str(err) + '. Waiting for ' + str(retryIn) + ' seconds before trying again...') |
|||
sleep(retryIn) |
|||
|
|||
if __name__ == "__main__": |
|||
main() |
@ -0,0 +1,47 @@ |
|||
from setuptools import setup, find_packages |
|||
import pathlib |
|||
|
|||
here = pathlib.Path(__file__).parent.resolve() |
|||
|
|||
# Get the long description from the README file |
|||
long_description = (here / 'README.md').read_text(encoding='utf-8') |
|||
|
|||
setup( |
|||
name='python-dvr', |
|||
|
|||
version='0.0.0', |
|||
|
|||
description='Python library for configuring a wide range of IP cameras which use the NETsurveillance ActiveX plugin XMeye SDK', |
|||
|
|||
long_description=long_description, |
|||
long_description_content_type='text/markdown', |
|||
|
|||
url='https://github.com/NeiroNx/python-dvr/', |
|||
|
|||
author='NeiroN', |
|||
|
|||
classifiers=[ |
|||
'Development Status :: 3 - Alpha', |
|||
|
|||
'Intended Audience :: Developers', |
|||
'Topic :: Multimedia :: Video :: Capture', |
|||
|
|||
'License :: OSI Approved :: MIT License', |
|||
|
|||
'Programming Language :: Python :: 3', |
|||
'Programming Language :: Python :: 3.6', |
|||
'Programming Language :: Python :: 3.7', |
|||
'Programming Language :: Python :: 3.8', |
|||
'Programming Language :: Python :: 3.9', |
|||
'Programming Language :: Python :: 3 :: Only', |
|||
], |
|||
|
|||
py_modules=["dvrip", "DeviceManager", "asyncio_dvrip"], |
|||
|
|||
python_requires='>=3.6', |
|||
|
|||
project_urls={ |
|||
'Bug Reports': 'https://github.com/NeiroNx/python-dvr/issues', |
|||
'Source': 'https://github.com/NeiroNx/python-dvr', |
|||
}, |
|||
) |
@ -0,0 +1,217 @@ |
|||
from time import sleep |
|||
from dvrip import DVRIPCam, SomethingIsWrongWithCamera |
|||
from pathlib import Path |
|||
import subprocess |
|||
import json |
|||
from datetime import datetime |
|||
|
|||
|
|||
class SolarCam: |
|||
cam = None |
|||
logger = None |
|||
|
|||
def __init__(self, host_ip, user, password, logger): |
|||
self.logger = logger |
|||
self.cam = DVRIPCam( |
|||
host_ip, |
|||
user=user, |
|||
password=password, |
|||
) |
|||
|
|||
def login(self, num_retries=10): |
|||
for i in range(num_retries): |
|||
try: |
|||
self.logger.debug("Try login...") |
|||
self.cam.login() |
|||
self.logger.debug( |
|||
f"Success! Connected to Camera. Waiting few seconds to let Camera fully boot..." |
|||
) |
|||
# waiting until camera is ready |
|||
sleep(10) |
|||
return |
|||
except SomethingIsWrongWithCamera: |
|||
self.logger.debug("Could not connect...Camera could be offline") |
|||
self.cam.close() |
|||
|
|||
if i == 9: |
|||
raise ConnectionRefusedError( |
|||
f"Could not connect {num_retries} times...aborting" |
|||
) |
|||
sleep(2) |
|||
|
|||
def logout(self): |
|||
self.cam.close() |
|||
|
|||
def get_time(self): |
|||
return self.cam.get_time() |
|||
|
|||
def set_time(self, time=None): |
|||
if time is None: |
|||
time = datetime.now() |
|||
return self.cam.set_time(time=time) |
|||
|
|||
def get_local_files(self, start, end, filetype): |
|||
return self.cam.list_local_files(start, end, filetype) |
|||
|
|||
def dump_local_files( |
|||
self, files, blacklist_path, download_dir, target_filetype=None |
|||
): |
|||
with open(f"{blacklist_path}.dmp", "a") as outfile: |
|||
for file in files: |
|||
target_file_path = self.generateTargetFilePath( |
|||
file["FileName"], download_dir |
|||
) |
|||
outfile.write(f"{target_file_path}\n") |
|||
|
|||
if target_filetype: |
|||
target_file_path_convert = self.generateTargetFilePath( |
|||
file["FileName"], download_dir, extention=f"{target_filetype}" |
|||
) |
|||
outfile.write(f"{target_file_path_convert}\n") |
|||
|
|||
def generateTargetFilePath(self, filename, downloadDir, extention=""): |
|||
fileExtention = Path(filename).suffix |
|||
filenameSplit = filename.split("/") |
|||
filenameDisk = f"{filenameSplit[3]}_{filenameSplit[5][:8]}".replace(".", "-") |
|||
targetPathClean = f"{downloadDir}/{filenameDisk}" |
|||
|
|||
if extention != "": |
|||
return f"{targetPathClean}{extention}" |
|||
|
|||
return f"{targetPathClean}{fileExtention}" |
|||
|
|||
def convertFile(self, sourceFile, targetFile): |
|||
if ( |
|||
subprocess.run( |
|||
f"ffmpeg -framerate 15 -i {sourceFile} -b:v 1M -c:v libvpx-vp9 -c:a libopus {targetFile}", |
|||
stdout=subprocess.DEVNULL, |
|||
stderr=subprocess.DEVNULL, |
|||
shell=True, |
|||
).returncode |
|||
!= 0 |
|||
): |
|||
self.logger.debug(f"Error converting video. Check {sourceFile}") |
|||
|
|||
self.logger.debug(f"File successfully converted: {targetFile}") |
|||
Path(sourceFile).unlink() |
|||
self.logger.debug(f"Orginal file successfully deleted: {sourceFile}") |
|||
|
|||
def save_files(self, download_dir, files, blacklist=None, target_filetype=None): |
|||
self.logger.debug(f"Start downloading files") |
|||
|
|||
for file in files: |
|||
target_file_path = self.generateTargetFilePath( |
|||
file["FileName"], download_dir |
|||
) |
|||
|
|||
target_file_path_convert = None |
|||
if target_filetype: |
|||
target_file_path_convert = self.generateTargetFilePath( |
|||
file["FileName"], download_dir, extention=f"{target_filetype}" |
|||
) |
|||
|
|||
if Path(f"{target_file_path}").is_file(): |
|||
self.logger.debug(f"File already exists: {target_file_path}") |
|||
continue |
|||
|
|||
if ( |
|||
target_file_path_convert |
|||
and Path(f"{target_file_path_convert}").is_file() |
|||
): |
|||
self.logger.debug( |
|||
f"Converted file already exists: {target_file_path_convert}" |
|||
) |
|||
continue |
|||
|
|||
if blacklist: |
|||
if target_file_path in blacklist: |
|||
self.logger.debug(f"File is on the blacklist: {target_file_path}") |
|||
continue |
|||
if target_file_path_convert and target_file_path_convert in blacklist: |
|||
self.logger.debug( |
|||
f"File is on the blacklist: {target_file_path_convert}" |
|||
) |
|||
continue |
|||
|
|||
self.logger.debug(f"Downloading {target_file_path}...") |
|||
self.cam.download_file( |
|||
file["BeginTime"], file["EndTime"], file["FileName"], target_file_path |
|||
) |
|||
self.logger.debug(f"Finished downloading {target_file_path}...") |
|||
|
|||
if target_file_path_convert: |
|||
self.logger.debug(f"Converting {target_file_path_convert}...") |
|||
self.convertFile(target_file_path, target_file_path_convert) |
|||
self.logger.debug(f"Finished converting {target_file_path_convert}.") |
|||
|
|||
self.logger.debug(f"Finish downloading files") |
|||
|
|||
def move_cam(self, direction, step=5): |
|||
match direction: |
|||
case "up": |
|||
self.cam.ptz_step("DirectionUp", step=step) |
|||
case "down": |
|||
self.cam.ptz_step("DirectionDown", step=step) |
|||
case "left": |
|||
self.cam.ptz_step("DirectionLeft", step=step) |
|||
case "right": |
|||
self.cam.ptz_step("DirectionRight", step=step) |
|||
case _: |
|||
self.logger.debug(f"No direction found") |
|||
|
|||
def mute_cam(self): |
|||
print( |
|||
self.cam.send( |
|||
1040, |
|||
{ |
|||
"fVideo.Volume": [ |
|||
{"AudioMode": "Single", "LeftVolume": 0, "RightVolume": 0} |
|||
], |
|||
"Name": "fVideo.Volume", |
|||
}, |
|||
) |
|||
) |
|||
|
|||
def set_volume(self, volume): |
|||
print( |
|||
self.cam.send( |
|||
1040, |
|||
{ |
|||
"fVideo.Volume": [ |
|||
{ |
|||
"AudioMode": "Single", |
|||
"LeftVolume": volume, |
|||
"RightVolume": volume, |
|||
} |
|||
], |
|||
"Name": "fVideo.Volume", |
|||
}, |
|||
) |
|||
) |
|||
|
|||
def get_battery(self): |
|||
data = self.cam.send_custom( |
|||
1610, |
|||
{"Name": "OPTUpData", "OPTUpData": {"UpLoadDataType": 5}}, |
|||
size=260, |
|||
)[87:-2].decode("utf-8") |
|||
json_data = json.loads(data) |
|||
return { |
|||
"BatteryPercent": json_data["Dev.ElectCapacity"]["percent"], |
|||
"Charging": json_data["Dev.ElectCapacity"]["electable"], |
|||
} |
|||
|
|||
def get_storage(self): |
|||
# get available storage in gb |
|||
storage_result = [] |
|||
data = self.cam.send(1020, {"Name": "StorageInfo"}) |
|||
for storage_index, storage in enumerate(data["StorageInfo"]): |
|||
for partition_index, partition in enumerate(storage["Partition"]): |
|||
s = { |
|||
"Storage": storage_index, |
|||
"Partition": partition_index, |
|||
"RemainingSpace": int(partition["RemainSpace"], 0) / 1024, |
|||
"TotalSpace": int(partition["TotalSpace"], 0) / 1024, |
|||
} |
|||
storage_result.append(s) |
|||
return storage_result |
@ -0,0 +1,244 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
from dvrip import DVRIPCam |
|||
from telnetlib import Telnet |
|||
import argparse |
|||
import datetime |
|||
import json |
|||
import os |
|||
import socket |
|||
import time |
|||
import requests |
|||
import zipfile |
|||
|
|||
TELNET_PORT = 4321 |
|||
ARCHIVE_URL = "https://github.com/widgetii/xmupdates/raw/main/archive" |
|||
|
|||
""" |
|||
Tested on XM boards: |
|||
IPG-53H20PL-S 53H20L_S39 00002532 |
|||
IPG-80H20PS-S 50H20L 00022520 |
|||
IVG-85HF20PYA-S HI3516EV200_50H20AI_S38 000559A7 |
|||
IVG-85HG50PYA-S HI3516EV300_85H50AI 000529B2 |
|||
|
|||
Issues with: "armbenv: can't load library 'libdvr.so'" |
|||
IPG-50HV20PES-S 50H20L_18EV200_S38 00018520 |
|||
""" |
|||
|
|||
# downgrade archive (mainly Yandex.Disk) |
|||
# https://www.cctvsp.ru/articles/obnovlenie-proshivok-dlya-ip-kamer-ot-xiong-mai |
|||
|
|||
XMV4 = { |
|||
"envtool": "XmEnv", |
|||
"flashes": [ |
|||
"0x00EF4017", |
|||
"0x00EF4018", |
|||
"0x00C22017", |
|||
"0x00C22018", |
|||
"0x00C22019", |
|||
"0x00C84017", |
|||
"0x00C84018", |
|||
"0x001C7017", |
|||
"0x001C7018", |
|||
"0x00207017", |
|||
"0x00207018", |
|||
"0x000B4017", |
|||
"0x000B4018", |
|||
], |
|||
} |
|||
|
|||
|
|||
def down(template, filename): |
|||
t = template.copy() |
|||
t['downgrade'] = filename |
|||
return t |
|||
|
|||
|
|||
# Borrowed from InstallDesc |
|||
conf = { |
|||
"000559A7": down(XMV4, "General_IPC_HI3516EV200_50H20AI_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20200507_all.bin"), |
|||
"000529B2": down(XMV4, "General_IPC_HI3516EV300_85H50AI_Nat_dss_OnvifS_HIK_V5_00_R02_20200507.bin"), |
|||
"000529E9": down(XMV4, "hacked_from_HI3516EV300_85H50AI.bin"), |
|||
} |
|||
|
|||
|
|||
def add_flashes(desc, swver): |
|||
board = conf.get(swver) |
|||
if board is None: |
|||
return |
|||
|
|||
fls = [] |
|||
for i in board["flashes"]: |
|||
fls.append({"FlashID": i}) |
|||
desc["SupportFlashType"] = fls |
|||
|
|||
|
|||
def get_envtool(swver): |
|||
board = conf.get(swver) |
|||
if board is None: |
|||
return "armbenv" |
|||
|
|||
return board["envtool"] |
|||
|
|||
|
|||
def make_zip(filename, data): |
|||
zipf = zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) |
|||
zipf.writestr("InstallDesc", data) |
|||
zipf.close() |
|||
|
|||
|
|||
def check_port(host_ip, port): |
|||
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|||
result_of_check = a_socket.connect_ex((host_ip, port)) |
|||
return result_of_check == 0 |
|||
|
|||
|
|||
def extract_gen(swver): |
|||
return swver.split(".")[3] |
|||
|
|||
|
|||
def cmd_armebenv(swver): |
|||
envtool = get_envtool(swver) |
|||
return { |
|||
"Command": "Shell", |
|||
"Script": f"{envtool} -s xmuart 0; {envtool} -s telnetctrl 1", |
|||
} |
|||
|
|||
|
|||
def cmd_telnetd(port): |
|||
return { |
|||
"Command": "Shell", |
|||
"Script": f"busybox telnetd -F -p {port} -l /bin/sh", |
|||
} |
|||
|
|||
|
|||
def cmd_backup(): |
|||
return [ |
|||
{ |
|||
"Command": "Shell", |
|||
"Script": "mount -o nolock 95.217.179.189:/srv/ro /utils/", |
|||
}, |
|||
{"Command": "Shell", "Script": "/utils/ipctool -w"}, |
|||
] |
|||
|
|||
|
|||
def downgrade_old_version(cam, buildtime, swver): |
|||
milestone = datetime.date(2020, 5, 7) |
|||
dto = datetime.datetime.strptime(buildtime, "%Y-%m-%d %H:%M:%S") |
|||
if dto.date() > milestone: |
|||
print( |
|||
f"Current firmware date {dto.date()}, but it needs to be no more than" |
|||
f" {milestone}\nConsider downgrade and only then continue.\n\n" |
|||
) |
|||
a = input("Are you sure to overwrite current firmware without backup (y/n)? ") |
|||
if a == "y": |
|||
board = conf.get(swver) |
|||
if board is None: |
|||
print(f"{swver} firmware is not supported yet") |
|||
return False |
|||
|
|||
print("DOWNGRADING\n") |
|||
url = f"{ARCHIVE_URL}/{swver}/{board['downgrade']}" |
|||
print(f"Downloading {url}") |
|||
r = requests.get(url, allow_redirects=True) |
|||
if r.status_code != requests.codes.ok: |
|||
print("Something went wrong") |
|||
return False |
|||
|
|||
open('upgrade.bin', 'wb').write(r.content) |
|||
print(f"Upgrading...") |
|||
cam.upgrade('upgrade.bin') |
|||
print("Completed. Wait a minute and then rerun") |
|||
return False |
|||
|
|||
return False |
|||
return True |
|||
|
|||
|
|||
def open_telnet(host_ip, port, **kwargs): |
|||
make_telnet = kwargs.get("telnet", False) |
|||
make_backup = kwargs.get("backup", False) |
|||
user = kwargs.get("username", "admin") |
|||
password = kwargs.get("password", "") |
|||
|
|||
cam = DVRIPCam(host_ip, user=user, password=password) |
|||
if not cam.login(): |
|||
print(f"Cannot connect {host_ip}") |
|||
return |
|||
upinfo = cam.get_upgrade_info() |
|||
hw = upinfo["Hardware"] |
|||
sysinfo = cam.get_system_info() |
|||
swver = extract_gen(sysinfo["SoftWareVersion"]) |
|||
print(f"Modifying camera {hw}, firmware {swver}") |
|||
if not downgrade_old_version(cam, sysinfo["BuildTime"], swver): |
|||
cam.close() |
|||
return |
|||
|
|||
print(f"Firmware generation {swver}") |
|||
|
|||
desc = { |
|||
"Hardware": hw, |
|||
"DevID": f"{swver}1001000000000000", |
|||
"CompatibleVersion": 2, |
|||
"Vendor": "General", |
|||
"CRC": "1ce6242100007636", |
|||
} |
|||
upcmd = [] |
|||
if make_telnet: |
|||
upcmd.append(cmd_telnetd(port)) |
|||
elif make_backup: |
|||
upcmd = cmd_backup() |
|||
else: |
|||
upcmd.append(cmd_armebenv(swver)) |
|||
desc["UpgradeCommand"] = upcmd |
|||
add_flashes(desc, swver) |
|||
|
|||
zipfname = "upgrade.bin" |
|||
make_zip(zipfname, json.dumps(desc, indent=2)) |
|||
cam.upgrade(zipfname) |
|||
cam.close() |
|||
os.remove(zipfname) |
|||
|
|||
if make_backup: |
|||
print("Check backup") |
|||
return |
|||
|
|||
if not make_telnet: |
|||
port = 23 |
|||
print("Waiting for camera is rebooting...") |
|||
|
|||
for i in range(10): |
|||
time.sleep(4) |
|||
if check_port(host_ip, port): |
|||
tport = f" {port}" if port != 23 else "" |
|||
print(f"Now use 'telnet {host_ip}{tport}' to login") |
|||
return |
|||
|
|||
print("Something went wrong") |
|||
return |
|||
|
|||
|
|||
def main(): |
|||
parser = argparse.ArgumentParser() |
|||
parser.add_argument("hostname", help="Camera IP address or hostname") |
|||
parser.add_argument( |
|||
"-u", "--username", default="admin", help="Username for camera login" |
|||
) |
|||
parser.add_argument( |
|||
"-p", "--password", default="", help="Password for camera login" |
|||
) |
|||
parser.add_argument( |
|||
"-b", "--backup", action="store_true", help="Make backup to the cloud" |
|||
) |
|||
parser.add_argument( |
|||
"-t", |
|||
"--telnet", |
|||
action="store_true", |
|||
help="Open telnet port without rebooting camera", |
|||
) |
|||
args = parser.parse_args() |
|||
open_telnet(args.hostname, TELNET_PORT, **vars(args)) |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
main() |
Loading…
Reference in new issue