Initial commit: reocon camera dashboard project
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
*.py[codz]
|
||||||
|
__pycache__/
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.pdm-python
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
{"ip": "192.168.1.21"},
|
||||||
|
{"ip": "192.168.1.22"},
|
||||||
|
{"ip": "192.168.1.23"},
|
||||||
|
{"ip": "192.168.1.24"},
|
||||||
|
{"ip": "192.168.1.25"},
|
||||||
|
{"ip": "192.168.1.26"},
|
||||||
|
{"ip": "192.168.1.27"},
|
||||||
|
{"ip": "192.168.1.28"},
|
||||||
|
{"ip": "192.168.1.29"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from reocon.cameras import get_camera_infos_df
|
||||||
|
from reocon.dashboard import app
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
handlers=[logging.StreamHandler()]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_console():
|
||||||
|
logger.info("Running in console mode")
|
||||||
|
df = get_camera_infos_df()
|
||||||
|
print(df.to_string(index=False))
|
||||||
|
|
||||||
|
|
||||||
|
def run_web():
|
||||||
|
logger.info("Starting Flask web dashboard")
|
||||||
|
app.run(debug=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "console":
|
||||||
|
run_console()
|
||||||
|
else:
|
||||||
|
run_web()
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
# This file is @generated by PDM.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
groups = ["default"]
|
||||||
|
strategy = ["inherit_metadata"]
|
||||||
|
lock_version = "4.5.0"
|
||||||
|
content_hash = "sha256:f61adc0c5e82bcf9e3b6ab15b27f0cb43326bd156b9b283d737db17585811f15"
|
||||||
|
|
||||||
|
[[metadata.targets]]
|
||||||
|
requires_python = "==3.14.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blinker"
|
||||||
|
version = "1.9.0"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "Fast, simple object-to-object and broadcast signaling"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
|
||||||
|
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2026.4.22"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"},
|
||||||
|
{file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.7"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"},
|
||||||
|
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"},
|
||||||
|
{file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"},
|
||||||
|
{file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.4.0"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "Composable command line interface toolkit"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"colorama; platform_system == \"Windows\"",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81"},
|
||||||
|
{file = "click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
summary = "Cross-platform colored terminal text."
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "platform_system == \"Windows\""
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "3.1.3"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "A simple framework for building complex web applications."
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"blinker>=1.9.0",
|
||||||
|
"click>=8.1.3",
|
||||||
|
"importlib-metadata>=3.6.0; python_version < \"3.10\"",
|
||||||
|
"itsdangerous>=2.2.0",
|
||||||
|
"jinja2>=3.1.2",
|
||||||
|
"markupsafe>=2.1.1",
|
||||||
|
"werkzeug>=3.1.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"},
|
||||||
|
{file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.15"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"},
|
||||||
|
{file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.2.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Safely pass data to untrusted environments and back."
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
|
||||||
|
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "A very fast and expressive template engine."
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"MarkupSafe>=2.0",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
|
||||||
|
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.3"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
|
||||||
|
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.5"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
summary = "Fundamental package for array computing in Python"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version >= \"3.14\""
|
||||||
|
files = [
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:889ca2c072315de638a5194a772aa1fa2df92bdd6175f6a222d4784040424b61"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:89e89304fb1f8c3f0ecfa4a7d48f311dd79771336a940e920159d643d1307e77"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:144fcc5a3a17679b2b82543b4a2d8dd29937230a7af13232b5f753872feb6361"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:398bb16772b265b9fa5c07b07072646ea97137c10ffb62a9a087b277fc825c29"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb352e7b8876da1249e72254736d6c58c505fa4e58a3d7e30efca241ca9ca9ce"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7341b08ff8124d7353939778e2707b8732d03c78c1c30e0815aba2dacbe1245a"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:deb01226f012539f3945261ffe1c10aec081a0fa0a5c925419933c70f3ae2d23"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d888bdf7335f76878c3c7b264ac1ff089863e211ec81249f9fb5795c2183dc25"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-win32.whl", hash = "sha256:15f90d1256e9b2320aff24fde44815b787ab6d7c49a1a11bfd8138b321c5f080"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4bd2cd4ef9c0afa87de73723c0a33c0edff62143e1432917458e26d3d195d87f"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:db304568c650e9d7039744d3575d0d287754debb2057d7c7b8cdfdc2c487a957"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de2883e0d2c63eae1bab1a84b390dca74aabb3d20ea1f5d58f360853c83abf3"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:06760fe73ae5005008748d182de612c733542af3cde063d532cd2127561b27be"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:4b51a01745cb04cc19278482207444b4d30728ce91c28d27a3bfae5fc6ff24c7"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a05636d7937d0936f271e5ba957fa8d746b5be3c2025caa1a2508f4fe521d40"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b86f56048ed09c3bbe48962a7dff077c2fd3274f8cf981800f3b38eac49cc3"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:130d58151c4db23e9fa860b84784e219a3aa3e030acc88a493ea37006c4dfd4c"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d475afc8cbe935ff5944f753d863bba774d7f4e1feaaa4102901e3e053ca5963"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-win32.whl", hash = "sha256:27f4a6dc26353a860b348961b9aa9e009835688b435cfa105e873b8dc2c726f5"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-win_amd64.whl", hash = "sha256:76ac6e90f5e226011c88f9b7040a4bcae612518bc7e9adc127e697a13b28ad1a"},
|
||||||
|
{file = "numpy-2.4.5-cp314-cp314t-win_arm64.whl", hash = "sha256:7c392e2c1bf596701d3c6832be7567eab5d5b0a13865036c33365ee097d37f8b"},
|
||||||
|
{file = "numpy-2.4.5.tar.gz", hash = "sha256:ca670567a5683b7c1670ec03e0ddd5862e10934e92a70751d68d7b7b74ca7f9f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "3.0.3"
|
||||||
|
requires_python = ">=3.11"
|
||||||
|
summary = "Powerful data structures for data analysis, time series, and statistics"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"numpy>=1.26.0; python_version < \"3.14\"",
|
||||||
|
"numpy>=2.3.3; python_version >= \"3.14\"",
|
||||||
|
"python-dateutil>=2.8.2",
|
||||||
|
"tzdata; sys_platform == \"emscripten\"",
|
||||||
|
"tzdata; sys_platform == \"win32\"",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1"},
|
||||||
|
{file = "pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1"},
|
||||||
|
{file = "pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pysocks"
|
||||||
|
version = "1.7.1"
|
||||||
|
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
summary = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
||||||
|
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
summary = "Extensions to the standard Python datetime module"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"six>=1.5",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||||
|
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.2"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
|
||||||
|
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.2"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "YAML parser and emitter for Python"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reolinkapi"
|
||||||
|
version = "0.4.1"
|
||||||
|
requires_python = ">=3.12.4"
|
||||||
|
summary = "Reolink Camera API client written in Python 3"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"PySocks==1.7.1",
|
||||||
|
"PyYaml==6.0.2",
|
||||||
|
"requests>=2.32.3",
|
||||||
|
"setuptools",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "reolinkapi-0.4.1-py3-none-any.whl", hash = "sha256:b3091f49e44676917d35be1102fa1b7a94c598e10bc1c76d932545f10caed9de"},
|
||||||
|
{file = "reolinkapi-0.4.1.tar.gz", hash = "sha256:3dc9db05d138643e31c7e48e7b3d1c82c016e541f2de554985ebc85e30a62bbd"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.34.2"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "Python HTTP for Humans."
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"certifi>=2023.5.7",
|
||||||
|
"charset-normalizer<4,>=2",
|
||||||
|
"idna<4,>=2.5",
|
||||||
|
"urllib3<3,>=1.26",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
|
||||||
|
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "82.0.1"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "Most extensible Python build backend with support for C/C++ extension modules"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
|
||||||
|
{file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
summary = "Python 2 and 3 compatibility utilities"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
|
||||||
|
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.2"
|
||||||
|
requires_python = ">=2"
|
||||||
|
summary = "Provider of IANA time zone data"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "sys_platform == \"emscripten\" or sys_platform == \"win32\""
|
||||||
|
files = [
|
||||||
|
{file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"},
|
||||||
|
{file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.7.0"
|
||||||
|
requires_python = ">=3.10"
|
||||||
|
summary = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
|
||||||
|
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "3.1.8"
|
||||||
|
requires_python = ">=3.9"
|
||||||
|
summary = "The comprehensive WSGI web application library."
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"markupsafe>=2.1.1",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"},
|
||||||
|
{file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"},
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[project]
|
||||||
|
name = "reocon"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Default template for PDM package"
|
||||||
|
authors = [
|
||||||
|
{name = "Alexander Rossmanith", email = ""},
|
||||||
|
]
|
||||||
|
dependencies = ["reolinkapi>=0.4.1", "pandas>=3.0.3", "flask>=3.1.3", "python-dotenv>=1.2.2"]
|
||||||
|
requires-python = "==3.14.*"
|
||||||
|
readme = "README.md"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pdm]
|
||||||
|
distribution = false
|
||||||
|
|
||||||
|
[tool.pdm.scripts]
|
||||||
|
reocon = "python main.py"
|
||||||
|
reocon-console = "python main.py console"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from reolinkapi import Camera
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Any, Dict, Iterator, List, Mapping, Optional
|
||||||
|
from reocon.tz import offset_to_zone
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
USER: Optional[str] = os.getenv("CAMERA_USER")
|
||||||
|
PASSWORD: Optional[str] = os.getenv("CAMERA_PASSWORD")
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
CameraInfo = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def get_camera_infos_df() -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Load information for all configured cameras into a DataFrame.
|
||||||
|
"""
|
||||||
|
rows: List[CameraInfo] = list(iter_camera_infos())
|
||||||
|
df = pd.DataFrame(rows)
|
||||||
|
logger.info("Generated table with %d rows", len(df))
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def iter_camera_infos() -> Iterator[CameraInfo]:
|
||||||
|
"""
|
||||||
|
Yield camera information dictionaries for each configured camera.
|
||||||
|
"""
|
||||||
|
with open("cameras.json", encoding="utf-8") as f:
|
||||||
|
cameraspecs = json.load(f)
|
||||||
|
logger.debug("Loaded %d cameras from config", len(cameraspecs))
|
||||||
|
for c in cameraspecs:
|
||||||
|
yield get_camera_info(c["ip"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_camera_info(ip: str) -> CameraInfo:
|
||||||
|
"""
|
||||||
|
Query a single camera and return its information as a dict.
|
||||||
|
"""
|
||||||
|
logger.info("Querying camera at %s", ip)
|
||||||
|
cam = get_camera_by_ip(ip)
|
||||||
|
info: CameraInfo = {}
|
||||||
|
info["IP"] = cam.ip
|
||||||
|
info["Name"] = get_name(cam)
|
||||||
|
info["Time"] = get_time(cam)
|
||||||
|
info["DST"] = get_dst(cam)
|
||||||
|
info["TZ"] = get_tz(cam)
|
||||||
|
|
||||||
|
devinfo = get_device_info(cam)
|
||||||
|
info["Model"] = get_model(devinfo)
|
||||||
|
info["Hardware"] = get_hardware(devinfo)
|
||||||
|
info["Firmware"] = get_firmware(devinfo)
|
||||||
|
|
||||||
|
network_general = get_network_general(cam)
|
||||||
|
info["MAC"] = get_mac(network_general)
|
||||||
|
info["Static_IP"] = get_static_ip(network_general)
|
||||||
|
info["Static_Gateway"] = get_static_gateway(network_general)
|
||||||
|
info["Static_Mask"] = get_static_mask(network_general)
|
||||||
|
info["DNS_auto"] = get_dns_auto(network_general)
|
||||||
|
info["DNS_1"] = get_dns1(network_general)
|
||||||
|
info["DNS_2"] = get_dns2(network_general)
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def get_camera_by_ip(ip: str) -> Camera:
|
||||||
|
"""
|
||||||
|
Create a Camera client for the given IP using environment credentials.
|
||||||
|
"""
|
||||||
|
assert USER is not None and PASSWORD is not None, "Camera credentials not set in environment variables"
|
||||||
|
return Camera(ip, USER, PASSWORD)
|
||||||
|
|
||||||
|
|
||||||
|
def get_name(cam: Camera) -> str:
|
||||||
|
"""
|
||||||
|
Return the device name from the camera or 'N/A' on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = cam.get_device_name()[0]
|
||||||
|
if resp.get("code") == 0 and resp.get("cmd") == "GetDevName":
|
||||||
|
return resp["value"]["DevName"]["name"]
|
||||||
|
else:
|
||||||
|
logger.error("get_name(%s): %s", cam.ip, resp)
|
||||||
|
return "N/A"
|
||||||
|
except Exception:
|
||||||
|
logger.exception("get_name(%s)", cam.ip)
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_tz(cam: Camera) -> str:
|
||||||
|
"""
|
||||||
|
Return the camera timezone string or 'N/A' on error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = cam.get_dst()[0]
|
||||||
|
if resp.get("code") == 0 and resp.get("cmd") == "GetTime":
|
||||||
|
time = resp["value"]["Time"]
|
||||||
|
return offset_to_zone(time["timeZone"])
|
||||||
|
else:
|
||||||
|
logger.error("get_tz(%s): %s", cam.ip, resp)
|
||||||
|
return "N/A"
|
||||||
|
except Exception:
|
||||||
|
logger.exception("get_tz(%s)", cam.ip)
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_dst(cam: Camera) -> str:
|
||||||
|
"""
|
||||||
|
Return whether Daylight Saving Time is enabled on the camera.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = cam.get_dst()[0]
|
||||||
|
if resp.get("code") == 0 and resp.get("cmd") == "GetTime":
|
||||||
|
dst = resp["value"]["Dst"]
|
||||||
|
enabled = dst["enable"]
|
||||||
|
return "Yes" if enabled else "No"
|
||||||
|
else:
|
||||||
|
logger.error("get_dst(%s): %s", cam.ip, resp)
|
||||||
|
return "N/A"
|
||||||
|
except Exception:
|
||||||
|
logger.exception("get_dst(%s)", cam.ip)
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_time(cam: Camera) -> str:
|
||||||
|
"""
|
||||||
|
Return the formatted camera time or 'N/A' if unavailable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = cam.get_dst()[0]
|
||||||
|
if resp.get("code") == 0 and resp.get("cmd") == "GetTime":
|
||||||
|
time = resp["value"]["Time"]
|
||||||
|
return f"{time['hour']:02d}:{time['min']:02d}:{time['sec']:02d}"
|
||||||
|
else:
|
||||||
|
logger.error("get_time(%s): %s", cam.ip, resp)
|
||||||
|
return "N/A"
|
||||||
|
except Exception:
|
||||||
|
logger.exception("get_time(%s)", cam.ip)
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_info(cam: Camera) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Request device info from the camera and return the parsed response.
|
||||||
|
"""
|
||||||
|
body = [{"cmd": "GetDevInfo", "action": 0, "param": {}}]
|
||||||
|
try:
|
||||||
|
resp = cam._execute_command('GetDevInfo', body)
|
||||||
|
assert len(resp) == 1
|
||||||
|
resp = resp[0]
|
||||||
|
assert resp["code"] == 0
|
||||||
|
value = resp["value"]["DevInfo"]
|
||||||
|
return value
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("get_device_info(%s): %s", cam.ip, e)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_model(devinfo: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the camera model string from the device info.
|
||||||
|
"""
|
||||||
|
return devinfo.get("model", "N/A")
|
||||||
|
|
||||||
|
|
||||||
|
def get_hardware(devinfo: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the camera hardware revision from device info.
|
||||||
|
"""
|
||||||
|
return devinfo.get("hardVer", "N/A")
|
||||||
|
|
||||||
|
|
||||||
|
def get_firmware(devinfo: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the camera firmware version from device info.
|
||||||
|
"""
|
||||||
|
return devinfo.get("firmVer", "N/A")
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_general(cam: Camera) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return network settings from the camera or an empty dict on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp = cam.get_network_general()
|
||||||
|
assert len(resp) == 1
|
||||||
|
resp = resp[0]
|
||||||
|
assert resp["code"] == 0
|
||||||
|
value = resp["value"]["LocalLink"]
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
logger.exception("get_network_general(%s)", cam.ip)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_mac(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the MAC address from network info.
|
||||||
|
"""
|
||||||
|
return network_general.get("mac", "N/A")
|
||||||
|
|
||||||
|
|
||||||
|
def get_dns_auto(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return whether DNS auto is enabled.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
dns_auto = network_general["dns"]["auto"]
|
||||||
|
return "Yes" if dns_auto else "No"
|
||||||
|
except KeyError:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_dns1(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return DNS1 from network settings.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return network_general["dns"]["dns1"]
|
||||||
|
except KeyError:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_dns2(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return DNS2 from network settings.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return network_general["dns"]["dns2"]
|
||||||
|
except KeyError:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_static_gateway(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the configured static gateway.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return network_general["static"]["gateway"]
|
||||||
|
except KeyError:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_static_ip(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the configured static IP address.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return network_general["static"]["ip"]
|
||||||
|
except KeyError:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def get_static_mask(network_general: Mapping[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Return the configured static subnet mask.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return network_general["static"]["mask"]
|
||||||
|
except KeyError:
|
||||||
|
return "N/A"
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
Flask dashboard routes and streaming API for the Reocon camera dashboard.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Iterator, List
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, Response
|
||||||
|
from .cameras import iter_camera_infos
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
COLUMN_SETS = {
|
||||||
|
"home": ["IP", "Model", "Name", "Hardware", "Firmware"],
|
||||||
|
"network": ["IP", "Model", "Name", "MAC", "Static_IP", "Static_Gateway", "Static_Mask", "DNS_auto", "DNS_1", "DNS_2"],
|
||||||
|
"time": ["IP", "Model", "Name", "Time", "TZ", "DST"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_columns(page_name: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Return the visible columns for the requested dashboard page.
|
||||||
|
"""
|
||||||
|
return COLUMN_SETS.get(page_name, COLUMN_SETS["home"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index() -> str:
|
||||||
|
"""
|
||||||
|
Render the home dashboard page.
|
||||||
|
"""
|
||||||
|
return render_template("dashboard.html", current_page="home")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/network")
|
||||||
|
def network() -> str:
|
||||||
|
"""
|
||||||
|
Render the network dashboard page.
|
||||||
|
"""
|
||||||
|
return render_template("dashboard.html", current_page="network")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/time")
|
||||||
|
def time() -> str:
|
||||||
|
"""
|
||||||
|
Render the time dashboard page.
|
||||||
|
"""
|
||||||
|
return render_template("dashboard.html", current_page="time")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/cameras")
|
||||||
|
def api_cameras() -> Response:
|
||||||
|
"""
|
||||||
|
Stream camera row updates for the selected dashboard page.
|
||||||
|
"""
|
||||||
|
page_name = request.args.get("page", "home")
|
||||||
|
columns = _get_columns(page_name)
|
||||||
|
|
||||||
|
def event_stream() -> Iterator[str]:
|
||||||
|
"""
|
||||||
|
Yield server-sent events for each camera row as it is queried.
|
||||||
|
"""
|
||||||
|
yield f"data: {json.dumps({'type': 'header', 'columns': columns})}\n\n"
|
||||||
|
for row in iter_camera_infos():
|
||||||
|
filtered = {column: row.get(column, "") for column in columns}
|
||||||
|
yield f"data: {json.dumps({'type': 'row', 'row': filtered, 'columns': columns})}\n\n"
|
||||||
|
yield "event: done\ndata: {}\n\n"
|
||||||
|
|
||||||
|
return Response(event_stream(), mimetype="text/event-stream")
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
body {
|
||||||
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-nav {
|
||||||
|
background: rgba(13, 110, 253, 0.08);
|
||||||
|
border: 1px solid rgba(13, 110, 253, 0.18);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
padding: 0.4rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-nav .nav-link {
|
||||||
|
border-radius: 1rem;
|
||||||
|
margin: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-nav .nav-link.active {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #0d6efd;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-nav .nav-link:not(.active) {
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-status {
|
||||||
|
background: rgba(13, 110, 253, 0.12);
|
||||||
|
border: 1px solid rgba(13, 110, 253, 0.22);
|
||||||
|
color: #0d6efd;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-row td {
|
||||||
|
opacity: 0.75;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const tableHead = document.getElementById("camera-table-head");
|
||||||
|
const tableBody = document.getElementById("camera-table-body");
|
||||||
|
const loadingStatus = document.getElementById("loading-status");
|
||||||
|
|
||||||
|
const showLoading = (message) => {
|
||||||
|
if (loadingStatus) {
|
||||||
|
loadingStatus.textContent = message;
|
||||||
|
loadingStatus.style.display = "block";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideLoading = () => {
|
||||||
|
if (loadingStatus) {
|
||||||
|
loadingStatus.style.display = "none";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadError = (message) => {
|
||||||
|
if (loadingStatus) {
|
||||||
|
loadingStatus.textContent = message;
|
||||||
|
loadingStatus.classList.remove("alert-info");
|
||||||
|
loadingStatus.classList.add("alert-danger");
|
||||||
|
loadingStatus.style.display = "block";
|
||||||
|
}
|
||||||
|
tableHead.innerHTML = "";
|
||||||
|
tableBody.innerHTML = `<tr class="table-danger"><td colspan="100">${message}</td></tr>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createHeader = (columns) => {
|
||||||
|
const headerRow = document.createElement("tr");
|
||||||
|
columns.forEach((column) => {
|
||||||
|
const th = document.createElement("th");
|
||||||
|
th.textContent = column;
|
||||||
|
headerRow.appendChild(th);
|
||||||
|
});
|
||||||
|
tableHead.innerHTML = "";
|
||||||
|
tableHead.appendChild(headerRow);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addRow = (row, columns) => {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
columns.forEach((column) => {
|
||||||
|
const td = document.createElement("td");
|
||||||
|
td.textContent = row[column] ?? "";
|
||||||
|
tr.appendChild(td);
|
||||||
|
});
|
||||||
|
tableBody.appendChild(tr);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCameraData = () => {
|
||||||
|
const source = new EventSource(`/api/cameras?page=${encodeURIComponent(currentPage)}`);
|
||||||
|
|
||||||
|
source.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === "header") {
|
||||||
|
createHeader(data.columns);
|
||||||
|
tableBody.innerHTML = "";
|
||||||
|
} else if (data.type === "row") {
|
||||||
|
addRow(data.row, data.columns);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.addEventListener("done", () => {
|
||||||
|
hideLoading();
|
||||||
|
source.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
source.onerror = (event) => {
|
||||||
|
loadError("Unable to load camera information.");
|
||||||
|
source.close();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCameraData();
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Reocon Camera Dashboard</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard.css') }}">
|
||||||
|
</head>
|
||||||
|
<body class="container">
|
||||||
|
<h1>Reocon Camera Dashboard</h1>
|
||||||
|
<nav class="dashboard-nav nav nav-pills nav-fill">
|
||||||
|
<a href="/" class="nav-link {% if current_page == 'home' %}active{% endif %}" id="home-tab2" data-bs-toggle="tab" type="button" role="tab" aria-selected="{{ 'true' if current_page == 'home' else 'false' }}">Home</a>
|
||||||
|
<a href="/network" class="nav-link {% if current_page == 'network' %}active{% endif %}" id="profile-tab2" data-bs-toggle="tab" type="button" role="tab" aria-selected="{{ 'true' if current_page == 'network' else 'false' }}">Network</a>
|
||||||
|
<a href="/time" class="nav-link {% if current_page == 'time' %}active{% endif %}" id="contact-tab2" data-bs-toggle="tab" type="button" role="tab" aria-selected="{{ 'true' if current_page == 'time' else 'false' }}">Time</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="loading-status" class="alert alert-info py-2 mb-3" role="status">
|
||||||
|
Loading camera information…
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped" id="camera-table">
|
||||||
|
<thead id="camera-table-head">
|
||||||
|
<tr><th>Loading...</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="camera-table-body">
|
||||||
|
<tr class="loading-row"><td colspan="10">Loading camera information…</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="/" class="btn btn-primary mt-3">Refresh</a>
|
||||||
|
<script>
|
||||||
|
const currentPage = "{{ current_page }}";
|
||||||
|
</script>
|
||||||
|
<script src="{{ url_for('static', filename='dashboard.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def offset_to_zone(offset_seconds: int) -> str:
|
||||||
|
"""
|
||||||
|
Convert offset seconds to canonical zone name.
|
||||||
|
Special handling for India Standard Time (UTC+05:30).
|
||||||
|
"""
|
||||||
|
hours = abs(offset_seconds) // 3600
|
||||||
|
minutes = (abs(offset_seconds) % 3600) // 60
|
||||||
|
sign = "+" if offset_seconds < 0 else "-"
|
||||||
|
return f"UTC{sign}{hours:02d}:{minutes:02d}"
|
||||||
Reference in New Issue
Block a user