commit d4847579e62d38e8f95650718ec1f0c1236c40ab Author: Alexander Rossmanith Date: Tue Jun 9 22:34:39 2026 +0530 Initial commit: reocon camera dashboard project diff --git a/.env b/.env new file mode 100644 index 0000000..70ac0e3 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +CAMERA_USER=admin +CAMERA_PASSWORD=BlueBoy#1314 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0dd47c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.py[codz] +__pycache__/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.pdm-python diff --git a/cameras.json b/cameras.json new file mode 100644 index 0000000..362d96f --- /dev/null +++ b/cameras.json @@ -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"} +] diff --git a/main.py b/main.py new file mode 100644 index 0000000..ec6b091 --- /dev/null +++ b/main.py @@ -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() diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..fa5203a --- /dev/null +++ b/pdm.lock @@ -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"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b36c5fc --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/reocon.code-workspace b/reocon.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/reocon.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/reocon/__init__.py b/reocon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reocon/cameras.py b/reocon/cameras.py new file mode 100644 index 0000000..4535316 --- /dev/null +++ b/reocon/cameras.py @@ -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" diff --git a/reocon/dashboard.py b/reocon/dashboard.py new file mode 100644 index 0000000..45c74cd --- /dev/null +++ b/reocon/dashboard.py @@ -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") diff --git a/reocon/static/dashboard.css b/reocon/static/dashboard.css new file mode 100644 index 0000000..8012c11 --- /dev/null +++ b/reocon/static/dashboard.css @@ -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; +} diff --git a/reocon/static/dashboard.js b/reocon/static/dashboard.js new file mode 100644 index 0000000..7bf1460 --- /dev/null +++ b/reocon/static/dashboard.js @@ -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 = `${message}`; + }; + + 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(); +}); diff --git a/reocon/templates/dashboard.html b/reocon/templates/dashboard.html new file mode 100644 index 0000000..22f26b3 --- /dev/null +++ b/reocon/templates/dashboard.html @@ -0,0 +1,40 @@ + + + + + + Reocon Camera Dashboard + + + + +

Reocon Camera Dashboard

+ + +
+ Loading camera information… +
+ +
+ + + + + + + +
Loading...
Loading camera information…
+
+ + Refresh + + + + diff --git a/reocon/tz.py b/reocon/tz.py new file mode 100644 index 0000000..f70c8c6 --- /dev/null +++ b/reocon/tz.py @@ -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}"