Browse Source

dos2unix *

master
Dmitry Ermakov 1 year ago
parent
commit
33d33de5dd
  1. 110
      AlarmServer.py
  2. 198
      ArduinoOSD.cpp
  3. 2286
      DeviceManager.py
  4. 24
      Dockerfile
  5. 42
      LICENSE
  6. 188
      NVR.py
  7. 22
      NVRVideoDownloader.json
  8. 178
      NVRVideoDownloader.py
  9. 1406
      README.md
  10. 1572
      asyncio_dvrip.py
  11. 92
      connect.py
  12. 254
      download-local-files.py
  13. 2248
      dvrip.py
  14. 34
      examples/socketio/Dockerfile
  15. 30
      examples/socketio/README.md
  16. 214
      examples/socketio/app.py
  17. 34
      examples/socketio/client.py
  18. 28
      examples/socketio/requirements.txt
  19. 240
      monitor.py
  20. 94
      setup.py
  21. 434
      solarcam.py
  22. 488
      telnet_opener.py

110
AlarmServer.py

@ -1,55 +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)
#!/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)

198
ArduinoOSD.cpp

@ -1,99 +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();
//заготовки
//'{"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();

2286
DeviceManager.py

File diff suppressed because it is too large

24
Dockerfile

@ -1,12 +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"]
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) 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.
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.

188
NVR.py

@ -1,94 +1,94 @@
from time import sleep, monotonic
from dvrip import DVRIPCam, SomethingIsWrongWithCamera
from pathlib import Path
import logging
class NVR:
nvr = None
logger = None
def __init__(self, host_ip, user, password, logger):
self.logger = logger
self.nvr = DVRIPCam(
host_ip,
user=user,
password=password,
)
if logger.level <= logging.DEBUG:
self.nvr.debug()
def login(self):
try:
self.logger.info(f"Connecting to NVR...")
self.nvr.login()
self.logger.info("Successfuly connected to NVR.")
return
except SomethingIsWrongWithCamera:
self.logger.error("Can't connect to NVR")
self.nvr.close()
def logout(self):
self.nvr.close()
def get_channel_statuses(self):
channel_statuses = self.nvr.get_channel_statuses()
if 'Ret' in channel_statuses:
return None
channel_titles = self.nvr.get_channel_titles()
if 'Ret' in channel_titles:
return None
for i in range(min(len(channel_statuses), len(channel_titles))):
channel_statuses[i]['Title'] = channel_titles[i]
channel_statuses[i]['Channel'] = i
return [c for c in channel_statuses if c['Status'] != '']
def get_local_files(self, channel, start, end, filetype):
return self.nvr.list_local_files(start, end, filetype, channel)
def generateTargetFileName(self, filename):
# My NVR's filename example: /idea0/2023-11-19/002/05.38.58-05.39.34[M][@69f17][0].h264
# You should check file names in your NVR and review the transformation
filenameSplit = filename.replace("][", "/").replace("[", "/").replace("]", "/").split("/")
return f"{filenameSplit[3]}_{filenameSplit[2]}_{filenameSplit[4]}{filenameSplit[-1]}"
def save_files(self, download_dir, files):
self.logger.info(f"Files downloading: start")
size_to_download = sum(int(f['FileLength'], 0) for f in files)
for file in files:
target_file_name = self.generateTargetFileName(file["FileName"])
target_file_path = f"{download_dir}/{target_file_name}"
size = int(file['FileLength'], 0)
size_to_download -= size
if Path(f"{target_file_path}").is_file():
self.logger.info(f" {target_file_name} file already exists, skipping download")
continue
self.logger.info(f" {target_file_name} [{size/1024:.1f} MBytes] downloading...")
time_dl = monotonic()
self.nvr.download_file(
file["BeginTime"], file["EndTime"], file["FileName"], target_file_path
)
time_dl = monotonic() - time_dl
speed = size / time_dl
self.logger.info(f" Done [{speed:.1f} KByte/s] {size_to_download/1024:.1f} MBytes more to download")
self.logger.info(f"Files downloading: done")
def list_files(self, files):
self.logger.info(f"Files listing: start")
for file in files:
target_file_name = self.generateTargetFileName(file["FileName"])
size = int(file['FileLength'], 0)
self.logger.info(f" {target_file_name} [{size/1024:.1f} MBytes]")
self.logger.info(f"Files listing: end")
from time import sleep, monotonic
from dvrip import DVRIPCam, SomethingIsWrongWithCamera
from pathlib import Path
import logging
class NVR:
nvr = None
logger = None
def __init__(self, host_ip, user, password, logger):
self.logger = logger
self.nvr = DVRIPCam(
host_ip,
user=user,
password=password,
)
if logger.level <= logging.DEBUG:
self.nvr.debug()
def login(self):
try:
self.logger.info(f"Connecting to NVR...")
self.nvr.login()
self.logger.info("Successfuly connected to NVR.")
return
except SomethingIsWrongWithCamera:
self.logger.error("Can't connect to NVR")
self.nvr.close()
def logout(self):
self.nvr.close()
def get_channel_statuses(self):
channel_statuses = self.nvr.get_channel_statuses()
if 'Ret' in channel_statuses:
return None
channel_titles = self.nvr.get_channel_titles()
if 'Ret' in channel_titles:
return None
for i in range(min(len(channel_statuses), len(channel_titles))):
channel_statuses[i]['Title'] = channel_titles[i]
channel_statuses[i]['Channel'] = i
return [c for c in channel_statuses if c['Status'] != '']
def get_local_files(self, channel, start, end, filetype):
return self.nvr.list_local_files(start, end, filetype, channel)
def generateTargetFileName(self, filename):
# My NVR's filename example: /idea0/2023-11-19/002/05.38.58-05.39.34[M][@69f17][0].h264
# You should check file names in your NVR and review the transformation
filenameSplit = filename.replace("][", "/").replace("[", "/").replace("]", "/").split("/")
return f"{filenameSplit[3]}_{filenameSplit[2]}_{filenameSplit[4]}{filenameSplit[-1]}"
def save_files(self, download_dir, files):
self.logger.info(f"Files downloading: start")
size_to_download = sum(int(f['FileLength'], 0) for f in files)
for file in files:
target_file_name = self.generateTargetFileName(file["FileName"])
target_file_path = f"{download_dir}/{target_file_name}"
size = int(file['FileLength'], 0)
size_to_download -= size
if Path(f"{target_file_path}").is_file():
self.logger.info(f" {target_file_name} file already exists, skipping download")
continue
self.logger.info(f" {target_file_name} [{size/1024:.1f} MBytes] downloading...")
time_dl = monotonic()
self.nvr.download_file(
file["BeginTime"], file["EndTime"], file["FileName"], target_file_path
)
time_dl = monotonic() - time_dl
speed = size / time_dl
self.logger.info(f" Done [{speed:.1f} KByte/s] {size_to_download/1024:.1f} MBytes more to download")
self.logger.info(f"Files downloading: done")
def list_files(self, files):
self.logger.info(f"Files listing: start")
for file in files:
target_file_name = self.generateTargetFileName(file["FileName"])
size = int(file['FileLength'], 0)
self.logger.info(f" {target_file_name} [{size/1024:.1f} MBytes]")
self.logger.info(f"Files listing: end")

22
NVRVideoDownloader.json

@ -1,11 +1,11 @@
{
"host_ip": "10.0.0.8",
"user": "admin",
"password": "mypassword",
"channel": 0,
"download_dir": "./download",
"start": "2023-11-19 6:22:34",
"end": "2023-11-19 6:23:09",
"just_list_files": false,
"log_level": "INFO"
}
{
"host_ip": "10.0.0.8",
"user": "admin",
"password": "mypassword",
"channel": 0,
"download_dir": "./download",
"start": "2023-11-19 6:22:34",
"end": "2023-11-19 6:23:09",
"just_list_files": false,
"log_level": "INFO"
}

178
NVRVideoDownloader.py

@ -1,89 +1,89 @@
from pathlib import Path
import os
import json
import logging
from collections import namedtuple
from NVR import NVR
def init_logger(log_level):
logger = logging.getLogger(__name__)
logger.setLevel(log_level)
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("NVRVIDEODOWNLOADER_CFG")
if config_path is None or not Path(config_path).exists():
config_path = "NVRVideoDownloader.json"
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"),
"channel": os.environ.get("CHANNEL"),
"download_dir": os.environ.get("DOWNLOAD_DIR"),
"start": os.environ.get("START"),
"end": os.environ.get("END"),
"just_list_files": os.environ.get("DUMP_LOCAL_FILES").lower() in ["true", "1", "y", "yes"],
"log_level": "INFO"
}
def main():
config = load_config()
logger = init_logger(config.log_level)
channel = config.channel;
start = config.start
end = config.end
just_list_files = config.just_list_files;
nvr = NVR(config.host_ip, config.user, config.password, logger)
try:
nvr.login()
channel_statuses = nvr.get_channel_statuses()
if channel_statuses:
channel_statuses_short = [{f"{c['Channel']}:{c['Title']}({c['ChnName']})"}
for c in channel_statuses if c['Status'] != 'NoConfig']
logger.info(f"Configured channels in NVR: {channel_statuses_short}")
videos = nvr.get_local_files(channel, start, end, "h264")
if videos:
size = sum(int(f['FileLength'], 0) for f in videos)
logger.info(f"Video files found: {len(videos)}. Total size: {size/1024:.1f}M")
Path(config.download_dir).parent.mkdir(
parents=True, exist_ok=True
)
if just_list_files:
nvr.list_files(videos)
else:
nvr.save_files(config.download_dir, videos)
else:
logger.info(f"No video files found")
nvr.logout()
except ConnectionRefusedError:
logger.error(f"Connection can't be established or got disconnected")
except TypeError as e:
print(e)
logger.error(f"Error while downloading a file")
except KeyError:
logger.error(f"Error while getting the file list")
if __name__ == "__main__":
main()
from pathlib import Path
import os
import json
import logging
from collections import namedtuple
from NVR import NVR
def init_logger(log_level):
logger = logging.getLogger(__name__)
logger.setLevel(log_level)
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("NVRVIDEODOWNLOADER_CFG")
if config_path is None or not Path(config_path).exists():
config_path = "NVRVideoDownloader.json"
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"),
"channel": os.environ.get("CHANNEL"),
"download_dir": os.environ.get("DOWNLOAD_DIR"),
"start": os.environ.get("START"),
"end": os.environ.get("END"),
"just_list_files": os.environ.get("DUMP_LOCAL_FILES").lower() in ["true", "1", "y", "yes"],
"log_level": "INFO"
}
def main():
config = load_config()
logger = init_logger(config.log_level)
channel = config.channel;
start = config.start
end = config.end
just_list_files = config.just_list_files;
nvr = NVR(config.host_ip, config.user, config.password, logger)
try:
nvr.login()
channel_statuses = nvr.get_channel_statuses()
if channel_statuses:
channel_statuses_short = [{f"{c['Channel']}:{c['Title']}({c['ChnName']})"}
for c in channel_statuses if c['Status'] != 'NoConfig']
logger.info(f"Configured channels in NVR: {channel_statuses_short}")
videos = nvr.get_local_files(channel, start, end, "h264")
if videos:
size = sum(int(f['FileLength'], 0) for f in videos)
logger.info(f"Video files found: {len(videos)}. Total size: {size/1024:.1f}M")
Path(config.download_dir).parent.mkdir(
parents=True, exist_ok=True
)
if just_list_files:
nvr.list_files(videos)
else:
nvr.save_files(config.download_dir, videos)
else:
logger.info(f"No video files found")
nvr.logout()
except ConnectionRefusedError:
logger.error(f"Connection can't be established or got disconnected")
except TypeError as e:
print(e)
logger.error(f"Error while downloading a file")
except KeyError:
logger.error(f"Error while getting the file list")
if __name__ == "__main__":
main()

1406
README.md

File diff suppressed because it is too large

1572
asyncio_dvrip.py

File diff suppressed because it is too large

92
connect.py

@ -1,46 +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()
#!/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()

254
download-local-files.py

@ -1,127 +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
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

2248
dvrip.py

File diff suppressed because it is too large

34
examples/socketio/Dockerfile

@ -1,17 +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"]
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"]

30
examples/socketio/README.md

@ -1,15 +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
```
### 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
```

214
examples/socketio/app.py

@ -1,107 +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)
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)

34
examples/socketio/client.py

@ -1,18 +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)
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')

28
examples/socketio/requirements.txt

@ -1,14 +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
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

240
monitor.py

@ -1,121 +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__":
#! /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()

94
setup.py

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

434
solarcam.py

@ -1,217 +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
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

488
telnet_opener.py

@ -1,244 +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()
#!/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