Initial commit: reocon camera dashboard project

This commit is contained in:
2026-06-09 22:34:39 +05:30
commit d4847579e6
14 changed files with 985 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
CAMERA_USER=admin
CAMERA_PASSWORD=BlueBoy#1314
+6
View File
@@ -0,0 +1,6 @@
*.py[codz]
__pycache__/
.mypy_cache/
.pytest_cache/
.ruff_cache/
.pdm-python
+11
View File
@@ -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"}
]
+30
View File
@@ -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()
Generated
+394
View File
@@ -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"},
]
+19
View File
@@ -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"
+8
View File
@@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}
View File
+264
View File
@@ -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"
+69
View File
@@ -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")
+57
View File
@@ -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;
}
+76
View File
@@ -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();
});
+40
View File
@@ -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>
+9
View File
@@ -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}"