Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b82c9af8c4 | |||
| f4e87910ab | |||
| b8b585d242 | |||
| b38067dd8c | |||
| ee6a150e3e | |||
| b30c85dead | |||
| 9f112e0903 | |||
| 95f132bdf8 | |||
| 6fe5db1adc |
@@ -0,0 +1,637 @@
|
||||
mqtt:
|
||||
enabled: false
|
||||
|
||||
detectors:
|
||||
coral:
|
||||
type: edgetpu
|
||||
device: pci
|
||||
|
||||
ffmpeg:
|
||||
hwaccel_args: preset-nvidia
|
||||
|
||||
|
||||
record:
|
||||
enabled: true
|
||||
alerts:
|
||||
retain:
|
||||
days: 30
|
||||
mode: motion
|
||||
detections:
|
||||
retain:
|
||||
days: 30
|
||||
mode: motion
|
||||
|
||||
continuous:
|
||||
days: 0
|
||||
motion:
|
||||
days: 3
|
||||
go2rtc:
|
||||
streams:
|
||||
# Backbasement:
|
||||
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.222:554/live/ch0#video=copy#audio=copy#audio=aac#hardware
|
||||
# SubBackBasement:
|
||||
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.222:554/live/ch1#video=copy#audio=copy#audio=aac#hardware
|
||||
# FrontBasement:
|
||||
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.223:554/live/ch0?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
|
||||
# SubFrontBasement:
|
||||
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.223:554/live/ch1?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
|
||||
# Eningeering:
|
||||
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.221:554/live/ch0?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
|
||||
# SubEngineering:
|
||||
# - ffmpeg:rtsp://tpadmin:Optimus0329@192.168.128.221:554/live/ch1?token=e6cf2225e8bf5fb028543898fab9a545#video=copy#audio=copy#audio=aac#hardware
|
||||
Driveway:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch12_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubDriveway:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch12_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
Porch:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch8_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubPorch:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch8_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
FenceGate:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch1_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubFenceGate:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch1_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
GardenArea:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch2_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubGardenArea:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch2_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
Backyard1:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch3_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubBackyard1:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch3_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
Backyard2:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch4_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubBackyard2:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch4_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
Backyard3:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch0_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubBackyard3:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch0_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
Backyard4:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch10_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubBackyard4:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch10_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
Backyard5:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch7_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubBackyard5:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch7_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
PatioArea:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch6_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubPatioArea:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch6_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
PoolArea:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch9_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubPoolArea:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch9_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SideHouse:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch11_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubSideHouse:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch11_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
BackSlider:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch5_0.264#video=copy#audio=copy#audio=aac#hardware
|
||||
SubBackSlider:
|
||||
- ffmpeg:rtsp://admin:Optimus0329@192.168.128.11:80/ch5_1.264#video=copy#audio=copy#audio=aac#hardware
|
||||
|
||||
|
||||
cameras:
|
||||
# name_of_your_camera: # <------ Name the camera
|
||||
# enabled: True
|
||||
# ffmpeg:
|
||||
# inputs:
|
||||
# - path: rtsp://10.0.10.10:554/rtsp # <----- The stream you want to use for detection
|
||||
# roles:
|
||||
# - detect
|
||||
# detect:
|
||||
# enabled: False # <---- disable detection until you have a working camera feed
|
||||
# width: 1280
|
||||
# height: 720
|
||||
|
||||
# back_basement:
|
||||
# enabled: true
|
||||
# ffmpeg:
|
||||
# inputs:
|
||||
# # - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/BackBasement
|
||||
# #input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
# - path: rtsp://127.0.0.1:8554/SubBackBasement
|
||||
# #input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - detect
|
||||
# detect:
|
||||
# enabled: true # <---- disable detection until you have a working camera feed
|
||||
# width: 1280
|
||||
# height: 720
|
||||
# fps: 5
|
||||
# motion:
|
||||
# enabled: true
|
||||
# threshold: 30
|
||||
# contour_area: 30
|
||||
# mask: 0.683,0.006,0.683,0.056,0.766,0.055,0.996,0.054,0.996,0.006
|
||||
# improve_contrast: true
|
||||
# snapshots:
|
||||
# enabled: true
|
||||
# timestamp: true
|
||||
# bounding_box: true
|
||||
# retain:
|
||||
# objects:
|
||||
# person: 7
|
||||
# front_basement:
|
||||
# enabled: true
|
||||
# ffmpeg:
|
||||
# inputs:
|
||||
# # - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/FrontBasement
|
||||
# #input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
# - path: rtsp://127.0.0.1:8554/SubFrontBasement
|
||||
# #input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - detect
|
||||
# detect:
|
||||
# enabled: true # <---- disable detection until you have a working camera feed
|
||||
# width: 1280
|
||||
# height: 720
|
||||
# fps: 5
|
||||
# motion:
|
||||
# enabled: true
|
||||
# threshold: 30
|
||||
# contour_area: 30
|
||||
# mask: 0.723,0,0.724,0.05,1,0.048,0.999,0.001
|
||||
# improve_contrast: true
|
||||
# snapshots:
|
||||
# enabled: true
|
||||
# timestamp: true
|
||||
# bounding_box: true
|
||||
# retain:
|
||||
# objects:
|
||||
# person: 7
|
||||
# record:
|
||||
# enabled: false
|
||||
# engineering:
|
||||
# enabled: true
|
||||
# ffmpeg:
|
||||
# inputs:
|
||||
# # - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Engineering
|
||||
# #input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
# - path: rtsp://127.0.0.1:8554/SubEngineering
|
||||
# #input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - detect
|
||||
# detect:
|
||||
# enabled: true # <---- disable detection until you have a working camera feed
|
||||
# width: 1280
|
||||
# height: 720
|
||||
# fps: 5
|
||||
# motion:
|
||||
# enabled: true
|
||||
# threshold: 14
|
||||
# contour_area: 15
|
||||
# improve_contrast: true
|
||||
# snapshots:
|
||||
# enabled: true
|
||||
# timestamp: true
|
||||
# bounding_box: true
|
||||
# retain:
|
||||
# objects:
|
||||
# person: 7
|
||||
# record:
|
||||
# enabled: false
|
||||
driveway:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Driveway
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubDriveway
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
zones:
|
||||
drivewayview:
|
||||
coordinates: 0,0.373,0,1,1,1,1,0.381,0.47,0.343,0.134,0.352
|
||||
loitering_time: 0
|
||||
friendly_name: DrivewayView
|
||||
porch:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Porch
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubPorch
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
fence_gate:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/FenceGate
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubFenceGate
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
zones:
|
||||
fencegate:
|
||||
coordinates:
|
||||
0,0.395,0.318,0.259,0.598,0.175,0.916,0.237,1,0.296,1,1,0.003,0.996
|
||||
loitering_time: 0
|
||||
friendly_name: FenceGate
|
||||
garden_area:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/GardenArea
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubGardenArea
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
backyard_1:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Backyard1
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubBackyard1
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
zones:
|
||||
backyard1:
|
||||
coordinates:
|
||||
0.001,0.274,0.124,0.162,0.228,0.143,0.382,0.131,0.555,0.133,0.714,0.16,0.854,0.196,0.998,0.253,0.998,0.996,0,0.992
|
||||
loitering_time: 0
|
||||
friendly_name: Backyard1
|
||||
backyard_2:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Backyard2
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubBackyard2
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
zones:
|
||||
backyard2:
|
||||
coordinates:
|
||||
0.002,0.147,0.086,0.106,0.172,0.091,0.271,0.078,0.386,0.077,0.499,0.077,0.601,0.086,0.698,0.099,0.787,0.117,0.859,0.087,0.995,0.155,0.998,0.992,0.001,0.995
|
||||
loitering_time: 0
|
||||
friendly_name: Backyard2
|
||||
backyard_3:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Backyard3
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubBackyard3
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
zones:
|
||||
backyard3:
|
||||
coordinates:
|
||||
0.001,0.337,0.166,0.332,0.327,0.342,0.419,0.344,0.51,0.355,0.603,0.363,0.692,0.374,0.771,0.381,0.848,0.389,0.951,0.405,0.997,0.423,0.998,0.996,0.003,0.994
|
||||
loitering_time: 0
|
||||
friendly_name: Backyard3
|
||||
motion:
|
||||
mask: 0.79,0.539,0.845,0.541,0.82,0.858,0.772,0.844
|
||||
backyard_4:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Backyard4
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubBackyard4
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
motion:
|
||||
mask:
|
||||
- 0.866,0.001,0.866,0.209,1,0.202,1,0
|
||||
- 0.001,0,0.249,0.001,0.249,0.045,0.002,0.044
|
||||
threshold: 60
|
||||
contour_area: 10
|
||||
improve_contrast: true
|
||||
backyard_5:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/Backyard5
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubBackyard5
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
motion:
|
||||
mask:
|
||||
- 0.029,0.31,0.13,0.305,0.134,0.54,0.032,0.544
|
||||
- 0.004,0.002,0.004,0.044,0.245,0.044,0.244,0
|
||||
threshold: 60
|
||||
contour_area: 10
|
||||
improve_contrast: true
|
||||
zones:
|
||||
backyard5:
|
||||
coordinates:
|
||||
0.001,0.305,0.043,0.264,0.113,0.194,0.19,0.128,0.271,0.109,0.371,0.089,0.525,0.057,0.709,0.057,0.89,0.076,0.997,0.122,0.997,0.995,0.002,0.998
|
||||
loitering_time: 0
|
||||
friendly_name: Backyard5
|
||||
patio_area:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/PatioArea
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubPatioArea
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
pool_area:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/PoolArea
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubPoolArea
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
motion:
|
||||
mask:
|
||||
0.43,0.146,0.704,0.194,0.532,0.808,0.199,0.704,0.002,0.559,0.252,0.282
|
||||
back_slider:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/BackSlider
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubBackSlider
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
side_house:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
# - path: rtsp://tpadmin:Optimus0329@192.168.128.225/stream1
|
||||
# - path: rtsp://127.0.0.1:8554/SideHouse
|
||||
# input_args: preset-rtsp-restream
|
||||
# roles:
|
||||
# - record
|
||||
- path: rtsp://127.0.0.1:8554/SubSideHouse
|
||||
input_args: preset-rtsp-restream
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
detect:
|
||||
enabled: true # <---- disable detection until you have a working camera feed
|
||||
width: 1280
|
||||
height: 720
|
||||
fps: 5
|
||||
snapshots:
|
||||
enabled: true
|
||||
timestamp: true
|
||||
bounding_box: true
|
||||
retain:
|
||||
objects:
|
||||
person: 7
|
||||
record:
|
||||
enabled: true
|
||||
motion:
|
||||
threshold: 60
|
||||
contour_area: 40
|
||||
improve_contrast: true
|
||||
mask:
|
||||
0.001,0.859,0.085,0.717,0.203,0.637,0.383,0.736,0.65,0.421,0.642,0.997,0.003,0.999
|
||||
zones:
|
||||
sidehouse:
|
||||
coordinates: 0.661,0.001,0.662,0.996,1,1,1,0
|
||||
loitering_time: 0
|
||||
friendly_name: SideHouse
|
||||
objects:
|
||||
filters:
|
||||
person: {}
|
||||
version: 0.17-0
|
||||
@@ -0,0 +1,32 @@
|
||||
# Edit this file to introduce tasks to be run by cron.
|
||||
#
|
||||
# Each task to run has to be defined through a single line
|
||||
# indicating with different fields when the task will be run
|
||||
# and what command to run for the task
|
||||
#
|
||||
# To define the time you can provide concrete values for
|
||||
# minute (m), hour (h), day of month (dom), month (mon),
|
||||
# and day of week (dow) or use '*' in these fields (for 'any').
|
||||
#
|
||||
# Notice that tasks will be started based on the cron's system
|
||||
# daemon's notion of time and timezones.
|
||||
#
|
||||
# Output of the crontab jobs (including errors) is sent through
|
||||
# email to the user the crontab file belongs to (unless redirected).
|
||||
#
|
||||
# For example, you can run a backup of all your user accounts
|
||||
# at 5 a.m every week with:
|
||||
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
|
||||
#
|
||||
# For more information see the manual pages of crontab(5) and cron(8)
|
||||
#
|
||||
# m h dom mon dow command
|
||||
30 0 * * * /cron_jobs/power/ipmi_poweroff.py -H 192.168.129.240 >> /var/log/cron_scripts.log 2>&1
|
||||
30 0 * * * /cron_jobs/power/ipmi_poweroff.py -H 192.168.129.241 >> /var/log/cron_scripts.log 2>&1
|
||||
0 1 * * * /cron_jobs/power/ilo_poweroff.py -H 192.168.129.242 -a ForceOff --no-serverinfo >> /var/log/cron_scripts.log 2>&1
|
||||
0 5 * * * /cron_jobs/system/ilo_poweron.py -H 192.168.129.242 -a On --no-serverinfo >> /var/log/cron_scripts.log 2>&1
|
||||
10 5 * * * /cron_jobs/power/ipmi_poweron.py -H 192.168.129.240 >> /var/log/cron_scripts.log 2>&1
|
||||
10 5 * * * /cron_jobs/power/ipmi_poweron.py -H 192.168.129.241 >> /var/log/cron_scripts.log 2>&1
|
||||
35 0 * * * /cron_jobs/system/update_system.py >> /var/log/cron_scripts.log 2>&1
|
||||
*/3 * * * * /cron_jobs/fancontrol/set_fanspeed.py -H 192.168.129.240 >> /var/log/cron_scripts.log 2>&1
|
||||
*/3 * * * * /cron_jobs/fancontrol/set_fanspeed.py -H 192.168.129.241 >> /var/log/cron_scripts.log 2>&1
|
||||
@@ -0,0 +1,4 @@
|
||||
[credentials]
|
||||
username=<username>
|
||||
password=<password>
|
||||
IPMIEK=0000000000000000000000000000000000000000
|
||||
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import datetime
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
|
||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
||||
|
||||
# IPMI settings (loaded at runtime from credentials file)
|
||||
IPMIUSER = None
|
||||
IPMIPW = None
|
||||
IPMIEK = DEFAULT_EK
|
||||
|
||||
# Fan / temperature settings
|
||||
FANSPEED = 20 # percent
|
||||
MAXTEMP = 37 # Celsius
|
||||
|
||||
def build_ipmi_base():
|
||||
base = ["ipmitool", "-I", IPMIINTERFACE, "-H", IPMIHOST, "-U", IPMIUSER, "-P", IPMIPW, "-y", IPMIEK]
|
||||
if IPMICIPHER is not None:
|
||||
base += ["-C", str(IPMICIPHER)]
|
||||
return base
|
||||
|
||||
def has_ipmi_session_error(stderr_text):
|
||||
s = (stderr_text or "").lower()
|
||||
markers = [
|
||||
"unable to establish ipmi v2 / rmcp+ session",
|
||||
"rakp",
|
||||
"session timeout",
|
||||
"authentication type",
|
||||
"invalid user name",
|
||||
]
|
||||
return any(marker in s for marker in markers)
|
||||
|
||||
def run_ipmi(args):
|
||||
base = build_ipmi_base()
|
||||
cmd = base + args
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if p.returncode != 0 or has_ipmi_session_error(p.stderr):
|
||||
raise RuntimeError(
|
||||
f"ipmitool failed: {' '.join(cmd)}\n"
|
||||
f"stdout: {(p.stdout or '').strip() or '<empty>'}\n"
|
||||
f"stderr: {(p.stderr or '').strip() or '<empty>'}"
|
||||
)
|
||||
return p.stdout
|
||||
|
||||
def run_ipmi_raw(args):
|
||||
base = build_ipmi_base()
|
||||
cmd = base + args
|
||||
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
return cmd, p.returncode, p.stdout, p.stderr
|
||||
|
||||
def log_systemd(tag, msg):
|
||||
try:
|
||||
subprocess.run(["systemd-cat", "-t", tag], input=msg + "\n", text=True, check=True)
|
||||
except Exception:
|
||||
# fallback to stdout
|
||||
ts_print(msg)
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def load_credentials(path):
|
||||
if not os.path.exists(path):
|
||||
raise RuntimeError(f"credentials file not found: {path}")
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path)
|
||||
|
||||
try:
|
||||
username = config.get("credentials", "username", fallback="root")
|
||||
password = config.get("credentials", "password")
|
||||
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
raise RuntimeError(f"error reading credentials file: {e}")
|
||||
|
||||
return username, password, encryption_key
|
||||
|
||||
def get_exhaust_temp():
|
||||
commands_to_try = [
|
||||
["sensor", "get", "Exhaust Temp"],
|
||||
["sdr", "type", "temperature"],
|
||||
["sensor", "list"],
|
||||
["sdr", "elist", "full"],
|
||||
]
|
||||
|
||||
debug_sections = []
|
||||
session_error_seen = False
|
||||
for args in commands_to_try:
|
||||
cmd, rc, out, err = run_ipmi_raw(args)
|
||||
if has_ipmi_session_error(err):
|
||||
session_error_seen = True
|
||||
cmd_text = " ".join(cmd)
|
||||
section = [
|
||||
f"Command: {cmd_text}",
|
||||
f"Return code: {rc}",
|
||||
"STDOUT:",
|
||||
out.strip() or "<empty>",
|
||||
"STDERR:",
|
||||
err.strip() or "<empty>",
|
||||
]
|
||||
debug_sections.append("\n".join(section))
|
||||
|
||||
# Parse explicit sensor-get output first.
|
||||
if args[:2] == ["sensor", "get"] and out:
|
||||
m = re.search(r"Sensor\s+Reading\s*:\s*(-?\d+(?:\.\d+)?)\b.*degrees\s*C", out, re.IGNORECASE)
|
||||
if m:
|
||||
return int(float(m.group(1)))
|
||||
|
||||
# Parse table outputs that have pipe-delimited sensor rows.
|
||||
for line in out.splitlines():
|
||||
parts = [p.strip() for p in line.split("|")]
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
|
||||
sensor_name = parts[0]
|
||||
normalized_name = " ".join(sensor_name.lower().split())
|
||||
if not (normalized_name == "exhaust temp" or normalized_name.startswith("exhaust temp ")):
|
||||
continue
|
||||
|
||||
for part in parts[1:]:
|
||||
m = re.search(r"(-?\d+(?:\.\d+)?)\s*degrees\s*C", part, re.IGNORECASE)
|
||||
if m:
|
||||
return int(float(m.group(1)))
|
||||
|
||||
if session_error_seen:
|
||||
raise RuntimeError(
|
||||
"Failed to establish an IPMI session. "
|
||||
"Check host/credentials, then try different options like "
|
||||
"--interface lan --cipher 3 or --interface lanplus --cipher 17.\n\n"
|
||||
+ "Debug output from attempted commands:\n\n"
|
||||
+ "\n\n---\n\n".join(debug_sections)
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
"Failed to read Exhaust Temp from ipmitool output.\n"
|
||||
+ "Debug output from attempted commands:\n\n"
|
||||
+ "\n\n---\n\n".join(debug_sections)
|
||||
)
|
||||
|
||||
def main():
|
||||
# parse CLI args (IPMI host can be overridden with -H/--host)
|
||||
parser = argparse.ArgumentParser(description="Set fan speed via IPMI")
|
||||
parser.add_argument("-H", "--host", required=True, help="IPMI host/IP")
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--credentials",
|
||||
default="/etc/credentials",
|
||||
help="Path to credentials file (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument("--interface", default="lanplus", choices=["lanplus", "lan"], help="IPMI interface (default: %(default)s)")
|
||||
parser.add_argument("--cipher", type=int, default=None, help="Optional IPMI cipher suite ID (e.g. 3 or 17)")
|
||||
args = parser.parse_args()
|
||||
|
||||
global IPMIHOST, IPMIINTERFACE, IPMICIPHER, IPMIUSER, IPMIPW, IPMIEK
|
||||
# set globals so run_ipmi() uses provided connection settings and credentials
|
||||
IPMIHOST = args.host
|
||||
IPMIINTERFACE = args.interface
|
||||
IPMICIPHER = args.cipher
|
||||
IPMIUSER, IPMIPW, IPMIEK = load_credentials(args.credentials)
|
||||
|
||||
ts_print(f"{IPMIHOST}: Fan speed check for host {IPMIHOST}")
|
||||
try:
|
||||
temp = get_exhaust_temp()
|
||||
ts_print(f"{IPMIHOST}: Exhaust temperature: {temp} C")
|
||||
except Exception as e:
|
||||
ts_print(f"{IPMIHOST}: Error reading temperature: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
speed_hex = format(FANSPEED, "x")
|
||||
|
||||
if temp > MAXTEMP:
|
||||
msg = f"{IPMIHOST}:Warning: Temperature is too high! Activating dynamic fan control! ({temp} C)"
|
||||
log_systemd("R710-IPMI-TEMP", msg)
|
||||
ts_print(msg)
|
||||
run_ipmi(["raw", "0x30", "0x30", "0x01", "0x01"])
|
||||
else:
|
||||
okmsg = f"{IPMIHOST}: Temperature is OK ({temp} C)"
|
||||
log_systemd("R710-IPMI-TEMP", okmsg)
|
||||
ts_print(okmsg)
|
||||
ts_print(f"{IPMIHOST}: Activating manual fan speeds! (requested %d%%)" % FANSPEED)
|
||||
run_ipmi(["raw", "0x30", "0x30", "0x01", "0x00"])
|
||||
run_ipmi(["raw", "0x30", "0x30", "0x02", "0xff", "0x" + speed_hex])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import getpass
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def run(cmd):
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {' '.join(cmd)}", file=sys.stderr)
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Run iLO REST commands (login, serverinfo, reboot)")
|
||||
p.add_argument("-H", "--host", required=True, help="iLO host/IP")
|
||||
p.add_argument("-a", "--action", default="ForceOff", choices=["ForceOff", "GracefulRestart", "Reset"],
|
||||
help="reboot action to run (default: ForceOff)")
|
||||
p.add_argument("--no-serverinfo", action="store_true", help="skip running serverinfo")
|
||||
args = p.parse_args()
|
||||
|
||||
# login
|
||||
ts_print(f"Sending {args.action} to {args.host}")
|
||||
run(['ilorest', 'login', args.host, '-u', '<username>', '-p', '<password>'])
|
||||
# optional serverinfo
|
||||
if not args.no_serverinfo:
|
||||
run(['ilorest', 'serverinfo'])
|
||||
# power off / reboot action
|
||||
run(['ilorest', 'reboot', args.action])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import getpass
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
def run(cmd):
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Command failed: {' '.join(cmd)}", file=sys.stderr)
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Login to iLO, show serverinfo and power on")
|
||||
p.add_argument("-H", "--host", required=True, help="iLO host/IP")
|
||||
p.add_argument("-a", "--action", default="On", choices=["On","Off","GracefulRestart","ForceOff","Reset"],
|
||||
help="power/reboot action to run (default: On)")
|
||||
p.add_argument("--no-serverinfo", action="store_true", help="skip running serverinfo")
|
||||
args = p.parse_args()
|
||||
|
||||
ts_print(f"Sending {args.action} to {args.host}")
|
||||
run(['ilorest', 'login', args.host, '-u', '<username>', '-p', '<password>'])
|
||||
if not args.no_serverinfo:
|
||||
run(['ilorest', 'serverinfo'])
|
||||
run(['ilorest', 'reboot', args.action])
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
IPMIUSER = "<username>"
|
||||
IPMIPW = "<password>"
|
||||
IPMIEK = "0000000000000000000000000000000000000000"
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power off")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
args = p.parse_args()
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", IPMIUSER,
|
||||
"-P", IPMIPW,
|
||||
"-y", IPMIEK,
|
||||
"power", "off",
|
||||
]
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
IPMIUSER = "<username>"
|
||||
IPMIPW = "<password>"
|
||||
IPMIEK = "0000000000000000000000000000000000000000"
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power off")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
args = p.parse_args()
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", IPMIUSER,
|
||||
"-P", IPMIPW,
|
||||
"-y", IPMIEK,
|
||||
"power", "on",
|
||||
]
|
||||
print(f"Sending power on to {args.host}")
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/python3
|
||||
import subprocess
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def generate_timestamp(fmt: Optional[str] = None, utc: bool = False) -> str:
|
||||
"""Return a timestamp string.
|
||||
|
||||
- If `fmt` is provided, return `datetime.strftime(fmt)`.
|
||||
- If `fmt` is None, return an ISO 8601 timestamp. When `utc` is True
|
||||
the timestamp ends with 'Z' (UTC). When `utc` is False it returns
|
||||
the local time ISO format.
|
||||
|
||||
Examples:
|
||||
- `generate_timestamp()` -> '2025-11-20T12:34:56Z'
|
||||
- `generate_timestamp("%Y-%m-%d %H:%M:%S")` -> '2025-11-20 12:34:56'
|
||||
"""
|
||||
if fmt:
|
||||
dt = datetime.now(timezone.utc) if utc else datetime.now()
|
||||
return dt.strftime(fmt)
|
||||
|
||||
dt = datetime.now(timezone.utc) if utc else datetime.now()
|
||||
if utc:
|
||||
# force trailing Z for UTC instead of +00:00
|
||||
return dt.replace(tzinfo=timezone.utc).isoformat().replace('+00:00', 'Z')
|
||||
return dt.isoformat()
|
||||
|
||||
print(f"Starting system update at {generate_timestamp()}")
|
||||
# Update package lists
|
||||
print(f"{generate_timestamp()}: Updating package lists...")
|
||||
subprocess.run(['sudo', 'apt', 'update'], check=True)
|
||||
# Upgrade packages
|
||||
print(f"{generate_timestamp()}: Upgrading packages...")
|
||||
subprocess.run(['sudo', 'apt', 'upgrade', '-y'], check=True)
|
||||
|
||||
# Check if reboot is required
|
||||
if os.path.exists('/var/run/reboot-required'):
|
||||
print(f"{generate_timestamp()}: Reboot required. Rebooting now...")
|
||||
subprocess.run(['sudo', 'reboot'])
|
||||
else:
|
||||
print(f"{generate_timestamp()}: No reboot required.")
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ def get_time():
|
||||
|
||||
def client_shutdown(client_type, clientid):
|
||||
lxc_filepath = "/usr/bin/lxc-stop"
|
||||
qemu_filepath = f"/usr/sbin/qm shutdown {clientid}"
|
||||
qemu_filepath = "/usr/sbin/qm"
|
||||
try:
|
||||
print(f'Stopping {clientid}')
|
||||
if client_type == 'vm':
|
||||
process_id = os.spawnv(os.P_NOWAIT, qemu_filepath, ['--skip-lock', 'true'])
|
||||
process_id = os.spawnv(os.P_NOWAIT, qemu_filepath, ['shutdown', clientid])
|
||||
elif client_type == 'lxc':
|
||||
process_id = os.spawnv(os.P_NOWAIT, lxc_filepath, [client_type, clientid])
|
||||
print(process_id)
|
||||
@@ -37,7 +37,6 @@ def main():
|
||||
print(f'Start time: {timestamp}')
|
||||
for clientid in client_list:
|
||||
client_shutdown(client_type, clientid)
|
||||
sleep(1)
|
||||
else:
|
||||
print(f'Successfully stopped client list.')
|
||||
|
||||
@@ -48,4 +47,4 @@ def main():
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
# sys.exit(0)
|
||||
# sys.exit(1)
|
||||
# sys.exit(1)
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# IPMI IP address
|
||||
IPMIHOST=<hostname>
|
||||
# IPMI Username
|
||||
IPMIUSER=<username
|
||||
# IPMI Password
|
||||
IPMIPW=<password>
|
||||
# Your IPMI Encryption Key
|
||||
IPMIEK=0<IPMI Encryption Key>
|
||||
# Fan Speed / utilisation in percentage, for example 9 for 9% utilisation
|
||||
# Please note that each fan can have a different rpm and will not all be the same speed
|
||||
FANSPEED=10
|
||||
|
||||
# TEMPERATURE
|
||||
# Change this to the temperature in Celsius you are comfortable with.
|
||||
# If the temperature goes above the set degrees it will send raw IPMI command to enable dynamic fan control
|
||||
MAXTEMP=30
|
||||
|
||||
# This variable sends a IPMI command to get the temperature, and outputs it as two digits.
|
||||
# Do not edit unless you know what you do.
|
||||
# Side note, if you are running ipmitool on the system you are controlling, you don't need to specify -H,-U,-P - from the OS installed on the host, ipmitool is assumed permitted. You only need host/user/pass for remote access.
|
||||
TEMP=$(ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK sdr type temperature |grep Ambient |grep degrees |grep -Po '\d{2}' | tail -1)
|
||||
|
||||
# Dont edit this, this converts decimal to hex
|
||||
SPEEDHEX=$( printf "%x" $FANSPEED )
|
||||
|
||||
if [[ $TEMP > $MAXTEMP ]];
|
||||
then
|
||||
printf "Warning: Temperature is too high! Activating dynamic fan control! ($TEMP C)" | systemd-cat -t R710-IPMI-TEMP
|
||||
echo "Warning: Temperature is too high! Activating dynamic fan control! ($TEMP C)"
|
||||
# This sets the fans to auto mode, so the motherboard will set it to a speed that it will need do cool the server down
|
||||
ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x01 0x01
|
||||
else
|
||||
printf "Temperature is OK ($TEMP C)" | systemd-cat -t R710-IPMI-TEMP
|
||||
printf "Activating manual fan speeds! (1560 RPM)"
|
||||
# This sets the fans to manual mode
|
||||
ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x01 0x00
|
||||
# This is where we set the slower, quite speed
|
||||
ipmitool -I lanplus -H $IPMIHOST -U $IPMIUSER -P $IPMIPW -y $IPMIEK raw 0x30 0x30 0x02 0xff 0x$SPEEDHEX
|
||||
fi
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power off
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
ipmitool -I lanplus -H 192.168.128.223 -U root -P Optimus0329 -y 0000000000000000000000000000000000000000 power on
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Client Type: $1"
|
||||
echo "Client IDs: $2"
|
||||
client_string=$2
|
||||
client_list=()
|
||||
|
||||
IFS=', ' read -a array <<< "${client_string}"; echo "${array[@]}"
|
||||
|
||||
for index in "${!array[@]}"
|
||||
do
|
||||
echo "Starting container: ${array[index]}"
|
||||
lxc-start ${array[index]}
|
||||
done
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
import urllib3
|
||||
import redfish
|
||||
|
||||
# Suppress SSL warnings for self-signed iLO certificates
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
parser = argparse.ArgumentParser(description="Power off an HPE server via iLO Redfish")
|
||||
parser.add_argument("hostname", help="iLO hostname or IP address")
|
||||
parser.add_argument("-c", "--credentials", default="/etc/ilo_credentials",
|
||||
help="Path to credentials file (default: /etc/ilo_credentials)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read credentials from file (chmod 600 recommended: sudo chmod 600 /etc/ilo_credentials)
|
||||
# File format:
|
||||
# [ilo]
|
||||
# username = root
|
||||
# password = yourpassword
|
||||
cred_path = args.credentials
|
||||
if not os.path.exists(cred_path):
|
||||
print(f"Error: credentials file not found: {cred_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(cred_path)
|
||||
try:
|
||||
LOGIN_ACCOUNT = config.get("ilo", "username", fallback="root")
|
||||
LOGIN_PASSWORD = config.get("ilo", "password")
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
BASE_URL = f"https://{args.hostname}"
|
||||
|
||||
REDFISHOBJ = redfish.RedfishClient(
|
||||
base_url=BASE_URL,
|
||||
username=LOGIN_ACCOUNT,
|
||||
password=LOGIN_PASSWORD,
|
||||
)
|
||||
REDFISHOBJ.login(auth="session")
|
||||
|
||||
# Discover the correct reset action URI from the system resource
|
||||
sys_response = REDFISHOBJ.get("/redfish/v1/Systems/1/")
|
||||
reset_uri = sys_response.dict["Actions"]["#ComputerSystem.Reset"]["target"]
|
||||
|
||||
body = {"ResetType": "PushPowerButton"} # iLO 4: PushPowerButton = graceful shutdown; use "ForceOff" for hard power cut
|
||||
response = REDFISHOBJ.post(reset_uri, body=body)
|
||||
|
||||
print(f"Status: {response.status}")
|
||||
if response.status >= 400:
|
||||
print(f"Error: {response.read}")
|
||||
|
||||
REDFISHOBJ.logout()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
||||
|
||||
|
||||
def load_credentials(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"Error: credentials file not found: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path)
|
||||
|
||||
try:
|
||||
username = config.get("credentials", "username", fallback="root")
|
||||
password = config.get("credentials", "password")
|
||||
encryption_key = config.get("credentials", "encryption_key", fallback=DEFAULT_EK)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return username, password, encryption_key
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power off")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
p.add_argument(
|
||||
"-c",
|
||||
"--credentials",
|
||||
default="credentials",
|
||||
help="Path to credentials file (default: credentials)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", ipmi_user,
|
||||
"-P", ipmi_pw,
|
||||
"-y", ipmi_ek,
|
||||
"power", "off",
|
||||
]
|
||||
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import configparser
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
def ts_print(msg):
|
||||
"""ts_print message prefixed with a timestamp (YYYY-MM-DD HH:MM:SS)."""
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"{now} {msg}")
|
||||
|
||||
DEFAULT_EK = "0000000000000000000000000000000000000000"
|
||||
|
||||
|
||||
def load_credentials(path):
|
||||
if not os.path.exists(path):
|
||||
print(f"Error: credentials file not found: {path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(path)
|
||||
|
||||
try:
|
||||
username = config.get("ilo", "username", fallback="root")
|
||||
password = config.get("ilo", "password")
|
||||
encryption_key = config.get("ilo", "encryption_key", fallback=DEFAULT_EK)
|
||||
except (configparser.NoSectionError, configparser.NoOptionError) as e:
|
||||
print(f"Error reading credentials file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return username, password, encryption_key
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Send IPMI power on")
|
||||
p.add_argument("-H", "--host", required=True, help="IPMI host address")
|
||||
p.add_argument(
|
||||
"-c",
|
||||
"--credentials",
|
||||
default="credentials",
|
||||
help="Path to credentials file (default: credentials)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
ipmi_user, ipmi_pw, ipmi_ek = load_credentials(args.credentials)
|
||||
|
||||
cmd = [
|
||||
"ipmitool",
|
||||
"-I", "lanplus",
|
||||
"-H", args.host,
|
||||
"-U", ipmi_user,
|
||||
"-P", ipmi_pw,
|
||||
"-y", ipmi_ek,
|
||||
"power", "on",
|
||||
]
|
||||
print(f"Sending power on to {args.host}")
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
if proc.returncode != 0:
|
||||
print("ipmitool failed:", proc.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(proc.returncode)
|
||||
print(proc.stdout.strip())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to read and parse Powerball numbers from a CSV file.
|
||||
"""
|
||||
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def read_powerball_csv(filepath: str) -> List[Dict]:
|
||||
"""
|
||||
Read a CSV file containing Powerball numbers.
|
||||
|
||||
Expected CSV format:
|
||||
Draw Date,Winning Numbers,Multiplier
|
||||
09/26/2020,11 21 27 36 62 24,3
|
||||
|
||||
Args:
|
||||
filepath: Path to the CSV file
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing parsed powerball data
|
||||
"""
|
||||
powerball_data = []
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
|
||||
for row_num, row in enumerate(reader, start=2): # start=2 because row 1 is header
|
||||
try:
|
||||
# Skip empty rows
|
||||
if not any(row.values()):
|
||||
continue
|
||||
|
||||
# Parse the row
|
||||
draw_date = datetime.strptime(row['Draw Date'].strip(), '%m/%d/%Y')
|
||||
|
||||
# Parse winning numbers - filter out empty strings
|
||||
numbers_str = row['Winning Numbers'].strip()
|
||||
winning_numbers = [int(n) for n in numbers_str.split() if n.strip()]
|
||||
|
||||
multiplier = int(row['Multiplier'].strip())
|
||||
|
||||
powerball_data.append({
|
||||
'draw_date': draw_date,
|
||||
'date_str': draw_date.strftime('%Y-%m-%d'),
|
||||
'winning_numbers': winning_numbers,
|
||||
'multiplier': multiplier
|
||||
})
|
||||
except ValueError as e:
|
||||
print(f"Warning: Skipping row {row_num} due to error: {e}")
|
||||
print(f" Row data: {row}")
|
||||
continue
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File '{filepath}' not found.")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error: Failed to read CSV file - {e}")
|
||||
return []
|
||||
|
||||
return powerball_data
|
||||
|
||||
|
||||
def display_powerball_data(data: List[Dict]) -> None:
|
||||
"""
|
||||
Display powerball data in a formatted table.
|
||||
|
||||
Args:
|
||||
data: List of powerball data dictionaries
|
||||
"""
|
||||
if not data:
|
||||
print("No data to display.")
|
||||
return
|
||||
|
||||
print(f"{'Draw Date':<12} {'Winning Numbers':<30} {'Multiplier':<10}")
|
||||
print("-" * 52)
|
||||
|
||||
for entry in data:
|
||||
numbers_str = ' '.join(map(str, entry['winning_numbers']))
|
||||
print(f"{entry['date_str']:<12} {numbers_str:<30} {entry['multiplier']:<10}")
|
||||
|
||||
|
||||
def analyze_most_frequent_numbers(data: List[Dict]) -> None:
|
||||
"""
|
||||
Analyze and display the most frequent number in each slot position.
|
||||
|
||||
Args:
|
||||
data: List of powerball data dictionaries
|
||||
"""
|
||||
if not data:
|
||||
print("No data to analyze.")
|
||||
return
|
||||
|
||||
# Initialize lists to hold numbers for each position
|
||||
num_positions = len(data[0]['winning_numbers'])
|
||||
position_numbers = [[] for _ in range(num_positions)]
|
||||
|
||||
# Collect all numbers for each position
|
||||
for entry in data:
|
||||
for position, number in enumerate(entry['winning_numbers']):
|
||||
position_numbers[position].append(number)
|
||||
|
||||
# Find most common number in each position
|
||||
print("\n" + "=" * 60)
|
||||
print("Most Frequent Numbers by Slot Position")
|
||||
print("=" * 60)
|
||||
|
||||
most_frequent = []
|
||||
for position, numbers in enumerate(position_numbers, start=1):
|
||||
counter = Counter(numbers)
|
||||
most_common_num, frequency = counter.most_common(1)[0]
|
||||
most_frequent.append(most_common_num)
|
||||
percentage = (frequency / len(numbers)) * 100
|
||||
print(f"Position {position}: {most_common_num:<3} (appears {frequency} times, {percentage:.1f}%)")
|
||||
|
||||
print("-" * 60)
|
||||
print(f"Most frequent numbers combined: {' '.join(map(str, most_frequent))}")
|
||||
|
||||
# Analyze most frequent number across first 5 positions combined
|
||||
print("\n" + "=" * 60)
|
||||
print("Additional Analysis")
|
||||
print("=" * 60)
|
||||
|
||||
# Combine all numbers from positions 1-5
|
||||
first_five_combined = []
|
||||
for i in range(min(5, num_positions)):
|
||||
first_five_combined.extend(position_numbers[i])
|
||||
|
||||
if first_five_combined:
|
||||
counter_first_five = Counter(first_five_combined)
|
||||
top_five_numbers = counter_first_five.most_common(5)
|
||||
print("Top 5 most drawn numbers in first 5 positions combined:")
|
||||
for rank, (number, frequency) in enumerate(top_five_numbers, start=1):
|
||||
percentage = (frequency / len(first_five_combined)) * 100
|
||||
print(f" {rank}. {number:<3} (appears {frequency} times, {percentage:.1f}%)")
|
||||
|
||||
# Analyze the sixth position (Powerball)
|
||||
most_common_sixth = None
|
||||
if num_positions >= 6:
|
||||
sixth_position_numbers = position_numbers[5]
|
||||
counter_sixth = Counter(sixth_position_numbers)
|
||||
most_common_sixth, freq_sixth = counter_sixth.most_common(1)[0]
|
||||
percentage_sixth = (freq_sixth / len(sixth_position_numbers)) * 100
|
||||
print(f"\nMost drawn number in position 6 (Powerball): {most_common_sixth} (appears {freq_sixth} times, {percentage_sixth:.1f}%)")
|
||||
|
||||
# Return statistics for prediction
|
||||
return {
|
||||
'top_five_combined': [num for num, freq in top_five_numbers] if first_five_combined else [],
|
||||
'most_common_powerball': most_common_sixth
|
||||
}
|
||||
|
||||
|
||||
def generate_suggested_combination(stats: dict) -> None:
|
||||
"""
|
||||
Generate a suggested combination based on statistical analysis.
|
||||
|
||||
Args:
|
||||
stats: Dictionary containing statistical analysis results
|
||||
"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Suggested Combination Based on Statistical Analysis")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n⚠️ DISCLAIMER: This is purely statistical analysis.")
|
||||
print("Past results do NOT predict future outcomes.")
|
||||
print("Each draw is random and independent.\n")
|
||||
|
||||
top_five = stats['top_five_combined']
|
||||
powerball = stats['most_common_powerball']
|
||||
|
||||
if len(top_five) >= 5 and powerball:
|
||||
# Use the top 5 most frequent numbers from positions 1-5
|
||||
suggested_numbers = sorted(top_five[:5])
|
||||
|
||||
print(f"Suggested white ball numbers: {' '.join(map(str, suggested_numbers))}")
|
||||
print(f"Suggested Powerball number: {powerball}")
|
||||
print()
|
||||
print(f"Complete combination: {' '.join(map(str, suggested_numbers))} + {powerball}")
|
||||
print()
|
||||
print("This combination is based on:")
|
||||
print(" • The 5 most frequently drawn numbers across all white ball positions")
|
||||
print(f" • The most frequently drawn Powerball number ({powerball})")
|
||||
else:
|
||||
print("Insufficient data to generate a suggestion.")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function."""
|
||||
# Default to 'powerball_numbers.csv' in the same directory as the script
|
||||
script_dir = Path(__file__).parent
|
||||
csv_file = script_dir / 'powerball_numbers.csv'
|
||||
|
||||
# You can also specify a different file path here
|
||||
# csv_file = Path('path/to/your/file.csv')
|
||||
|
||||
print(f"Reading Powerball numbers from: {csv_file}\n")
|
||||
|
||||
powerball_data = read_powerball_csv(str(csv_file))
|
||||
|
||||
if powerball_data:
|
||||
print(f"Successfully loaded {len(powerball_data)} records.\n")
|
||||
display_powerball_data(powerball_data)
|
||||
stats = analyze_most_frequent_numbers(powerball_data)
|
||||
generate_suggested_combination(stats)
|
||||
else:
|
||||
print("Failed to load powerball data.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/bin/python3
|
||||
"""
|
||||
Parallel rsync launcher.
|
||||
|
||||
Requires rsync on PATH (e.g. via MSYS/Cygwin on Windows).
|
||||
Runs multiple rsync invocations in parallel (one per file/entry).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
#DEFAULT_RSYNC_ARGS = ["-av", "--partial", "--compress", "--info=progress2"]
|
||||
DEFAULT_RSYNC_ARGS = ["-avP", "--partial"]
|
||||
|
||||
|
||||
def build_file_list(src: str) -> List[Path]:
|
||||
p = Path(src)
|
||||
if p.is_dir():
|
||||
# sync each top-level entry separately (files and immediate subdirs)
|
||||
return [x for x in p.iterdir()]
|
||||
# allow glob patterns
|
||||
if any(ch in src for ch in ["*", "?"]):
|
||||
return list(Path(".").glob(src))
|
||||
# single file
|
||||
return [p]
|
||||
|
||||
|
||||
def run_rsync(item: Path, dest: str, rsync_args: List[str], dry_run: bool) -> (Path, int, str, str):
|
||||
cmd = ["rsync", *rsync_args]
|
||||
if dry_run:
|
||||
cmd.append("--dry-run")
|
||||
# preserve relative path by sending the item name, not its parent
|
||||
cmd += [str(item), dest]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return item, proc.returncode, proc.stdout, proc.stderr
|
||||
except FileNotFoundError:
|
||||
return item, 127, "", "rsync not found on PATH"
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Transfer files using rsync in parallel.")
|
||||
ap.add_argument("src", help="Source file, directory or glob pattern")
|
||||
ap.add_argument("dest", help="Rsync destination (local path or user@host:/path)")
|
||||
ap.add_argument("-w", "--workers", type=int, default=4, help="Parallel rsync workers")
|
||||
ap.add_argument("-r", "--rsync-args", default=" ".join(DEFAULT_RSYNC_ARGS),
|
||||
help="Additional rsync args (quoted). Default: -a --partial --compress --progress")
|
||||
ap.add_argument("--dry-run", action="store_true", help="Don't transfer, just show what would happen")
|
||||
args = ap.parse_args()
|
||||
|
||||
items = build_file_list(args.src)
|
||||
if not items:
|
||||
print("No files found for:", args.src, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
rsync_args = args.rsync_args.split()
|
||||
results = []
|
||||
print(f"Starting {len(items)} rsync tasks with {args.workers} workers...")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=args.workers) as ex:
|
||||
futures = {ex.submit(run_rsync, item, args.dest, rsync_args, args.dry_run): item for item in items}
|
||||
for fut in as_completed(futures):
|
||||
item, code, out, err = fut.result()
|
||||
results.append((item, code, out, err))
|
||||
status = "OK" if code == 0 else f"ERR({code})"
|
||||
print(f"[{status}] {item}")
|
||||
|
||||
# summary
|
||||
failed = [r for r in results if r[1] != 0]
|
||||
print(f"\nCompleted: {len(results)} tasks, {len(failed)} failed.")
|
||||
if failed:
|
||||
print("Failures detail:")
|
||||
for item, code, out, err in failed:
|
||||
print(f"--- {item} (exit {code}) ---")
|
||||
if out:
|
||||
print(out.strip())
|
||||
if err:
|
||||
print(err.strip())
|
||||
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user