Browse Source

Sync repo from @nxn_neiron work

pull/1/head
Igor Zalatov 1 year ago
parent
commit
be3915fe1b
  1. 77
      .github/workflows/codeql.yml
  2. 24
      .github/workflows/main.yml
  3. 3
      .gitignore
  4. 55
      AlarmServer.py
  5. 99
      ArduinoOSD.cpp
  6. 1143
      DeviceManager.py
  7. 12
      Dockerfile
  8. 42
      LICENSE
  9. 705
      README.md
  10. 786
      asyncio_dvrip.py
  11. 46
      connect.py
  12. BIN
      doc/码流帧格式文档.pdf
  13. BIN
      doc/配置交换格式V2.0.pdf
  14. BIN
      doc/雄迈数字视频录像机接口协议V1.0.doc
  15. BIN
      doc/雄迈数字视频录像机接口协议_V1.0.0.pdf
  16. 127
      download-local-files.py
  17. 1124
      dvrip.py
  18. 17
      examples/socketio/Dockerfile
  19. 15
      examples/socketio/README.md
  20. 107
      examples/socketio/app.py
  21. 18
      examples/socketio/client.py
  22. 14
      examples/socketio/requirements.txt
  23. BIN
      images/osd-new.png
  24. BIN
      images/vixand.jpg
  25. BIN
      images/xm.jpg
  26. 121
      monitor.py
  27. 47
      setup.py
  28. 217
      solarcam.py
  29. 244
      telnet_opener.py

77
.github/workflows/codeql.yml

@ -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}}"

24
.github/workflows/main.yml

@ -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 }}

3
.gitignore

@ -0,0 +1,3 @@
*.pyc
*.old
.DS_Store

55
AlarmServer.py

@ -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)

99
ArduinoOSD.cpp

@ -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();

1143
DeviceManager.py

File diff suppressed because it is too large

12
Dockerfile

@ -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"]

42
LICENSE

@ -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.

705
README.md

@ -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
![screenshot](images/xm.jpg)
## 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)
```
![screenshot](images/vixand.jpg)
```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"})
```
![screenshot](images/osd-new.png)
## 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

786
asyncio_dvrip.py

@ -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

46
connect.py

@ -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()

BIN
doc/码流帧格式文档.pdf

Binary file not shown.

BIN
doc/配置交换格式V2.0.pdf

Binary file not shown.

BIN
doc/雄迈数字视频录像机接口协议V1.0.doc

Binary file not shown.

BIN
doc/雄迈数字视频录像机接口协议_V1.0.0.pdf

Binary file not shown.

127
download-local-files.py

@ -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

1124
dvrip.py

File diff suppressed because it is too large

17
examples/socketio/Dockerfile

@ -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"]

15
examples/socketio/README.md

@ -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
```

107
examples/socketio/app.py

@ -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)

18
examples/socketio/client.py

@ -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')

14
examples/socketio/requirements.txt

@ -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

BIN
images/osd-new.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
images/vixand.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
images/xm.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

121
monitor.py

@ -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()

47
setup.py

@ -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',
},
)

217
solarcam.py

@ -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

244
telnet_opener.py

@ -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…
Cancel
Save