├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ └── docker-image.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── DOCS.md ├── Dockerfile-for-github-action ├── LICENSE ├── README.md ├── assets ├── Alipay.png ├── Ha-mini-card.jpg ├── QQ_group.jpg ├── WeiChat.jpg ├── background.png ├── badge.svg ├── database.png ├── edit1.jpg ├── image-20230730135540291.png ├── image-20240514.jpg └── restart.jpg ├── config.yaml ├── docker-compose.yml ├── example.env ├── ha_addons_doc ├── Add-on教程.md └── img │ ├── add-repository.png │ ├── addon-running-status.png │ ├── addon-store.png │ ├── addons-page.png │ ├── configuration-ignore_user_id.png │ ├── configuration.png │ ├── find-addon.png │ ├── install-addon.png │ ├── installation-complete.png │ ├── installation-progress.png │ ├── refresh.png │ ├── repositories-menu.png │ ├── show-unused-options.png │ └── start-addon.png ├── icon.png ├── logo.png ├── repository.yaml ├── requirements.txt └── scripts ├── captcha.onnx ├── const.py ├── data_fetcher.py ├── main.py ├── onnx.py └── sensor_updator.py /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 向我报告问题,或者你遇到了问题,或者对功能进行请求 2 | description: 创建问题 3 | body: 4 | - type: textarea 5 | validations: 6 | required: true 7 | id: problem 8 | attributes: 9 | label: 问题描述 10 | description: >- 11 | 在此描述您遇到的问题,以便与维护者沟通。告诉我们您正在尝试做什么,以及发生了什么。 12 | 提供一个清晰简洁的问题描述。 13 | - type: markdown 14 | attributes: 15 | value: | 16 | ## 环境 17 | - type: input 18 | id: version 19 | validations: 20 | required: true 21 | attributes: 22 | label: 哪个版本存在此问题? 23 | description: > 24 | Releases发行版 25 | - type: dropdown 26 | validations: 27 | required: true 28 | id: selfTest 29 | attributes: 30 | label: 您是否手动登录国家电网网页并尝试获取数据? 31 | options: 32 | - 未尝试手动获取数据 33 | - 手动登录不成功 34 | - 已经手动登录国网并且可以正常获取数据 35 | - type: dropdown 36 | validations: 37 | required: true 38 | id: installation 39 | attributes: 40 | label: 您使用的是哪种安装方式? 41 | options: 42 | - Windows Docker环境 43 | - Linxu Docker环境 44 | - 群晖Docker环境 45 | - 其他环境请在附加信息说明 46 | - type: input 47 | validations: 48 | required: false 49 | attributes: 50 | label: Home Assistant core 哪个版本,安装方式, 51 | description: > 52 | Home Assistant 的版本,如 2021.8。 安装方式Hassos Docker 53 | - type: textarea 54 | id: logs 55 | attributes: 56 | label: 日志中是否有任何可能对我们有用的信息? 57 | description: 例如,docker logs -f <容器ID> 58 | - type: textarea 59 | id: additional 60 | attributes: 61 | label: 附加信息 62 | description: > 63 | 如果您有任何附加信息提供给我们,请在下面的字段中填写。 64 | 请注意,您可以通过拖放文件在下面的字段中附加截图或屏幕录制。 65 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'version of this branch' 8 | required: true 9 | type: string 10 | paths: 11 | - 'scripts/**' 12 | - 'Dockerfile-for-github-action' 13 | - 'requirements.txt' 14 | - 'example.env' 15 | - '.github/workflows/docker-image.yml' 16 | 17 | jobs: 18 | 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v1 27 | 28 | - name: Log into docker hub registry 29 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 30 | 31 | 32 | - name: Build and push Docker image 33 | run: | 34 | PLATFORMS=linux/arm64,linux/amd64 35 | DOCKER_IMAGE=arcw/sgcc_electricity 36 | ARCH_TAGS="arm64 amd64" 37 | VERSION=${{ inputs.version }} 38 | docker buildx build --build-arg VERSION=${VERSION} --platform $PLATFORMS -t $DOCKER_IMAGE:latest -t $DOCKER_IMAGE:${VERSION} --file Dockerfile-for-github-action --push . 39 | 40 | for ARCH in $ARCH_TAGS; do 41 | if [ "$ARCH" == "arm64" ]; then 42 | TAG_ARCH="aarch64" 43 | else 44 | TAG_ARCH=$ARCH 45 | fi 46 | docker buildx build --build-arg VERSION=${VERSION} --platform linux/${ARCH} -t ${DOCKER_IMAGE}-${TAG_ARCH}:latest -t ${DOCKER_IMAGE}-${TAG_ARCH}:${VERSION} --file Dockerfile-for-github-action --push . 47 | done 48 | 49 | - name: Log into Aliyun hub registry and push Docker image 50 | run: | 51 | echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.ALIYUN_USERNAME }} --password-stdin registry.cn-hangzhou.aliyuncs.com 52 | PLATFORMS=linux/arm64,linux/amd64 53 | DOCKER_IMAGE=registry.cn-hangzhou.aliyuncs.com/arcw/sgcc_electricity 54 | ARCH_TAGS="arm64 amd64" 55 | VERSION=${{ inputs.version }} 56 | docker buildx build --build-arg VERSION=${VERSION} --platform $PLATFORMS -t $DOCKER_IMAGE:latest -t $DOCKER_IMAGE:${VERSION} --file Dockerfile-for-github-action --push . 57 | 58 | for ARCH in $ARCH_TAGS; do 59 | if [ "$ARCH" == "arm64" ]; then 60 | TAG_ARCH="aarch64" 61 | else 62 | TAG_ARCH=$ARCH 63 | fi 64 | docker buildx build --build-arg VERSION=${VERSION} --platform linux/${ARCH} -t ${DOCKER_IMAGE}-${TAG_ARCH}:latest -t ${DOCKER_IMAGE}-${TAG_ARCH}:${VERSION} --file Dockerfile-for-github-action --push . 65 | done -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | config.yaml 3 | .env 4 | 5 | translations 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # poetry 103 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 104 | # This is especially recommended for binary packages to ensure reproducibility, and is more 105 | # commonly ignored for libraries. 106 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 107 | #poetry.lock 108 | 109 | # pdm 110 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 111 | #pdm.lock 112 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 113 | # in version control. 114 | # https://pdm.fming.dev/#use-with-ide 115 | .pdm.toml 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | .dmypy.json 149 | dmypy.json 150 | 151 | # Pyre type checker 152 | .pyre/ 153 | 154 | # pytype static type analyzer 155 | .pytype/ 156 | 157 | # Cython debug symbols 158 | cython_debug/ 159 | 160 | # PyCharm 161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 163 | # and can be added to the global gitignore or merged into this file. For a more nuclear 164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 165 | .idea/ 166 | *.db 167 | ./script/__pycache__ 168 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python 调试程序: 当前文件", 9 | "type": "debugpy", 10 | "request": "launch", 11 | // "program": "${file}", 12 | "program": "main.py", 13 | "console": "integratedTerminal", 14 | "cwd": "${workspaceFolder}\\scripts" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.6.4] - 2025-01-06 4 | 5 | ### Fixed 6 | 7 | - add-on功能优化. 8 | 9 | ## [v1.6.3] - 2025-01-05 10 | 11 | ### Added 12 | 13 | - 初步测试了addon功能. 14 | 15 | ### Fixed 16 | 17 | - 修复了一些小bug. 18 | 19 | [v1.6.4]: https://github.com/ARC-MX/sgcc_electricity_new/compare/v1.6.3...v1.6.4 20 | [v1.6.3]: https://github.com/ARC-MX/sgcc_electricity_new/releases/tag/v1.6.3 21 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # Home Assistant sgcc electricity new Add-on 配置和启动 2 | 3 | ## 配置和启动 4 | ### 1. 配置 5 | - 安装完成后,点击 "CONFIGURATION" 或 "配置" 标签 6 | - 根据需要修改配置参数 7 | ![配置Add-on](https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/refs/heads/master/ha_addons_doc/img/configuration.png) 8 | - 点击显示未使用的可选配置选项按钮,可以配置ignore_user_id等可选参数 9 | ![显示未使用的可选配置选项](https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/refs/heads/master/ha_addons_doc/img/show-unused-options.png) 10 | ![配置ignore_user_id](https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/refs/heads/master/ha_addons_doc/img/configuration-ignore_user_id.png) 11 | 12 | - 点击 "SAVE" 或 "保存" 保存配置 13 | 14 | ### 2. 启动 15 | - 点击 "Info" 或 "信息" 标签页 16 | - 点击 "START" 或 "启动" 启动 Add-on 17 | ![启动Add-on](https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/refs/heads/master/ha_addons_doc/img/start-addon.png) 18 | 19 | - 启动后,点击 "日志" 标签页,可以看到Add-on的运行状态 20 | ![Add-on运行状态](https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/refs/heads/master/ha_addons_doc/img/addon-running-status.png) 21 | 22 | ## 常见问题 23 | - 如果无法找到新添加的 Add-on,请尝试刷新页面 24 | - 如果安装失败,检查存储库地址是否正确 25 | - 遇到问题可以查看 "日志" 标签页的日志信息 -------------------------------------------------------------------------------- /Dockerfile-for-github-action: -------------------------------------------------------------------------------- 1 | FROM python:3.11.11-slim-bookworm as build 2 | 3 | ENV PYTHONDONTWRITEBYTECODE=1 4 | ENV PYTHONUNBUFFERED=1 5 | ENV SET_CONTAINER_TIMEZONE=true 6 | ENV CONTAINER_TIMEZONE=Asia/Shanghai 7 | ENV TZ=Asia/Shanghai 8 | 9 | 10 | ARG TARGETARCH 11 | ARG VERSION 12 | ENV VERSION=${VERSION} 13 | ENV PYTHON_IN_DOCKER='PYTHON_IN_DOCKER' 14 | 15 | COPY scripts/* /app/ 16 | WORKDIR /app 17 | 18 | RUN apt-get --allow-releaseinfo-change update \ 19 | && apt-get install -y --no-install-recommends jq chromium chromium-driver tzdata \ 20 | && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ 21 | && echo $TZ > /etc/timezone \ 22 | && dpkg-reconfigure --frontend noninteractive tzdata \ 23 | && rm -rf /var/lib/apt/lists/* \ 24 | && apt-get clean 25 | 26 | COPY ./requirements.txt /tmp/requirements.txt 27 | 28 | RUN mkdir /data \ 29 | && cd /tmp \ 30 | && python3 -m pip install --upgrade pip \ 31 | && PIP_ROOT_USER_ACTION=ignore pip install \ 32 | --disable-pip-version-check \ 33 | --no-cache-dir \ 34 | -r requirements.txt \ 35 | && rm -rf /tmp/* \ 36 | && pip cache purge \ 37 | && rm -rf /var/lib/apt/lists/* \ 38 | && rm -rf /var/log/* 39 | 40 | ENV LANG=C.UTF-8 41 | 42 | CMD ["python3","main.py"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **重要说明:**原作者@renhai-lab 已于2023年10将项目归档,原仓库不再更新。这个版本是在原仓库基础上大幅改动,在此向原作者表达谢意和致敬。验证码识别已经从最开始的在线商业API替换成离线神经网络检测版本,请使用本仓库的同学点个小星星,或者打赏鼓励。 2 | 3 | 添加微信通知后,我想这基本上就是这个插件的最终形态了,docker镜像压缩到300MB,后续可能只会在网站变动或者出问题才会更新,再次感谢大家的Star。 4 | 5 | **注意** 有很多新手都在提交验证码不能识别的相关issue,特在此统一说明:国网每天有登录限制,每天只能登录有限的几次,超过限制验证码识别成功也不会登录成功。因此,诸如[issue47](https://github.com/ARC-MX/sgcc_electricity_new/issues/47),[issue50](https://github.com/ARC-MX/sgcc_electricity_new/issues/50),[issue29](https://github.com/ARC-MX/sgcc_electricity_new/issues/29)这些都是这个问题,以后就不做回复了。 6 | 7 | 最近issue太多实在是回复不过来了,特此添加QQ交流群 8 | [关于创建QQ付费群的说明](https://github.com/ARC-MX/sgcc_electricity_new/issues/78) 9 | 10 | ### 支付宝&微信 打赏码 11 | 12 |

13 | 14 | 15 |

16 | 17 | # ⚡️国家电网电力获取 18 | 19 | [![Docker Image CI](https://github.com/ARC-MX/sgcc_electricity_new/actions/workflows/docker-image.yml/badge.svg)](https://github.com/ARC-MX/sgcc_electricity_new/actions/workflows/docker-image.yml) 20 | [![Image Size](https://img.shields.io/docker/image-size/arcw/sgcc_electricity)](https://hub.docker.com/r/arcw/sgcc_electricity) 21 | [![Docker Pull](https://img.shields.io/docker/pulls/arcw/sgcc_electricity?color=%2348BB78&logo=docker&label=pulls)](https://hub.docker.com/r/arcw/sgcc_electricity) 22 | 23 |

24 | mini-graph-card 25 | mini-graph-card 26 |

27 | 28 | ## 简介 29 | 30 | 本应用可以帮助你将国网的电费、用电量数据接入homeassistant,实现实时追踪家庭用电量情况;并且可以将每日用电量保存到数据库,历史有迹可循。具体提供两类数据: 31 | 32 | 1. 在homeassistant以实体显示: 33 | 34 | | 实体entity_id | 说明 | 35 | | -------------------------------------- | -------------------------------------------------- | 36 | | sensor.last_electricity_usage_xxxx | 最近一天用电量,单位KWH、度。 | 37 | | sensor.electricity_charge_balance_xxxx | 预付费显示电费余额,反之显示上月应交电费,单位元。 | 38 | | sensor.yearly_electricity_usage_xxxx | 今年总用电量,单位KWH、度。 | 39 | | sensor.yearly_electricity_charge_xxxx | 今年总用电费,单位元。 | 40 | | sensor.month_electricity_usage_xxxx | 最近一个月用电量,单位KWH、度。 | 41 | | sensor.month_electricity_charge_xxxx | 上月总用电费,单位元。 | 42 | 2. 可选,近三十天每日用电量数据(SQLite数据库) 43 | 数据库表名为 daily+userid ,在项目路径下有个homeassistant.db 的数据库文件就是; 44 | 如需查询可以用 45 | 46 | ``` 47 | "SELECT * FROM dailyxxxxxxxx;" 48 | ``` 49 | 50 | 得到如下结果: 51 | 52 | mini-graph-card 53 | 54 | ## 适用范围 55 | 56 | 1. 适用于除南方电网覆盖省份外的用户。即除广东、广西、云南、贵州、海南等省份的用户外,均可使用本应用获取电力、电费数据。 57 | 2. 不管是通过哪种哪种安装的homeassistant,只要可以运行python,有约1G硬盘空间和500M运行内存,都可以采用本仓库部署。 58 | 59 | 本镜像支持架构: 60 | 61 | > - `linux/amd64`:适用于 x86-64(amd64)架构的 Linux 系统,例如windows电脑。 62 | > - `linux/arm64`:适用于 ARMv8 架构的 Linux 系统,例如树莓派3+,N1盒子等。 63 | > - `linux/armv7`,暂不提供 ARMv7 架构的 Linux 系统,例如树莓派2,玩客云等,主要原因是onnx-runtime没有armv7版本的库,用户可以参考 [https://github.com/nknytk/built-onnxruntime-for-raspberrypi-linux.git](https://github.com/nknytk/built-onnxruntime-for-raspberrypi-linux.git)自行安装库然后编译docker镜像。 64 | 65 | ## 实现流程 66 | 67 | 通过python的selenium包获取国家电网的数据,通过homeassistant的提供的[REST API](https://developers.home-assistant.io/docs/api/rest/)将采用POST请求将实体状态更新到homeassistant。 68 | 69 | 国家电网添加了滑动验证码登录验证,我这边最早采取了调用商业API的方式,现在已经更新成了离线方案。利用Yolov3神经网络识别验证码,请大家放心使用。 70 | 71 | # 安装与部署 72 | 73 | ## 1)注册国家电网账户 74 | 75 | 首先要注册国家电网账户,绑定电表,并且可以手动查询电量 76 | 77 | 注册网址:[https://www.95598.cn/osgweb/login](https://www.95598.cn/osgweb/login) 78 | 79 | ## 2)获取HA token 80 | 81 | token获取方法参考[https://blog.csdn.net/qq_25886111/article/details/106282492](https://blog.csdn.net/qq_25886111/article/details/106282492) 82 | 83 | ## 3)docker镜像部署,速度快 84 | 85 | 1. 安装docker和homeassistant,[Homeassistant极简安装法](https://github.com/renhaiidea/easy-homeassistant)。 86 | 2. 克隆仓库 87 | 88 | ```bash 89 | git clone https://github.com/ARC-MX/sgcc_electricity_new.git 90 | # 如果github网络环境不好的话可以使用国内镜像,完全同步的,个人推荐使用国内镜像 91 | # git clone https://gitee.com/ARC-MX/sgcc_electricity_new.git 92 | cd sgcc_electricity_new 93 | ``` 94 | 95 | 3. 创建环境变量文件 96 | 97 | ```bash 98 | cp example.env .env 99 | vim .env # 参考以下文件编写.env文件 100 | ``` 101 | 102 | ```bash 103 | ### 以下项都需要修改 104 | ## 国网登录信息 105 | # 修改为自己的登录账号 106 | PHONE_NUMBER="xxx" 107 | # 修改为自己的登录密码 108 | PASSWORD="xxxx" 109 | # 排除指定用户ID,如果出现一些不想检测的ID或者有些充电、发电帐号、可以使用这个环境变量,如果有多个就用","分隔,","之间不要有空格 110 | IGNORE_USER_ID=xxxxxxx,xxxxxxx,xxxxxxx 111 | 112 | # SQLite 数据库配置 113 | # or False 不启用数据库储存每日用电量数据。 114 | ENABLE_DATABASE_STORAGE=True 115 | # 数据库名,默认为homeassistant 116 | DB_NAME="homeassistant.db" 117 | # COLLECTION_NAME默认为electricity_daily_usage_{国网用户id},不支持修改。 118 | 119 | ## homeassistant配置 120 | # 改为你的localhost为你的homeassistant地址 121 | HASS_URL="http://localhost:8123/" 122 | # homeassistant的长期令牌 123 | HASS_TOKEN="eyxxxxx" 124 | 125 | ## selenium运行参数 126 | # 任务开始时间,24小时制,例如"07:00”则为每天早上7点执行,第一次启动程序如果时间晚于早上7点则会立即执行一次,每隔12小时执行一次。 127 | JOB_START_TIME="07:00" 128 | # 每次操作等待时间,推荐设定范围为[2,30],该值表示每次点击网页后所要等待数据加载的时间,如果出现“no such element”诸如此类的错误可适当调大该值,如果硬件性能较好可以适当调小该值 129 | RETRY_WAIT_TIME_OFFSET_UNIT=15 130 | 131 | 132 | ## 记录的天数, 仅支持填写 7 或 30 133 | # 国网原本可以记录 30 天,现在不开通智能缴费只能查询 7 天造成错误 134 | DATA_RETENTION_DAYS=7 135 | 136 | ## 余额提醒 137 | # 是否缴费提醒 138 | RECHARGE_NOTIFY=Flase 139 | # 余额 140 | BALANCE=5.0 141 | # pushplus token 如果有多个就用","分隔,","之间不要有空格,单个就不要有"," 142 | PUSHPLUS_TOKEN=xxxxxxx,xxxxxxx,xxxxxxx 143 | ``` 144 | 145 | 4. 运行 146 | 147 | 我已经优化了镜像环境,将镜像的地址配置为阿里云,如果要使用docker hub的源可以将docker-compose.yml中 148 | image: registry.cn-hangzhou.aliyuncs.com/arcw/sgcc_electricity:latest 改为 arcw/sgcc_electricity:latest 149 | 150 | ```bash 151 | 运行获取传感器名称 152 | docker-compose up -d 153 | docker-compose logs sgcc_electricity_app 154 | ``` 155 | 156 | 运行成功应该显示如下日志: 157 | 158 | ```bash 159 | 2024-06-06 16:00:43 [INFO ] ---- 程序开始,当前仓库版本为1.x.x,仓库地址为https://github.com/ARC-MX/sgcc_electricity_new.git 160 | 2024-06-06 16:00:43 [INFO ] ---- enable_database_storage为false,不会储存到数据库 161 | 2024-06-06 16:00:43 [INFO ] ---- 当前登录的用户名为: xxxxxx,homeassistant地址为http://192.168.1.xx:8123/,程序将在每天00:00执行 162 | 2024-06-06 16:00:43 [INFO ] ---- 此次为首次运行,等待时间(FIRST_SLEEP_TIME)为10秒,可在.env中设置 163 | 2024-06-06 16:00:59 [INFO ] ---- Webdriver initialized. 164 | 2024-06-06 16:01:20 [INFO ] ---- Click login button. 165 | 2024-06-06 16:01:20 [INFO ] ---- Get electricity canvas image successfully. 166 | 2024-06-06 16:01:20 [INFO ] ---- Image CaptCHA distance is xxx. 167 | 2024-06-06 16:01:25 [INFO ] ---- Login successfully on https://www.95598.cn/osgweb/login 168 | 2024-06-06 16:01:33 [INFO ] ---- 将获取1户数据,user_id: ['xxxxxxx'] 169 | 2024-06-06 16:01:42 [INFO ] ---- Get electricity charge balance for xxxxxxx successfully, balance is xxx CNY. 170 | 2024-06-06 16:01:51 [INFO ] ---- Get year power usage for xxxxxxx successfully, usage is xxx kwh 171 | 2024-06-06 16:01:51 [INFO ] ---- Get year power charge for xxxxxxx successfully, yealrly charge is xxx CNY 172 | 2024-06-06 16:01:55 [INFO ] ---- Get month power charge for xxxxxxx successfully, 01 月 usage is xxx KWh, charge is xxx CNY. 173 | 2024-06-06 16:01:55 [INFO ] ---- Get month power charge for xxxxxxx successfully, 02 月 usage is xxx KWh, charge is xxx CNY. 174 | 2024-06-06 16:01:55 [INFO ] ---- Get month power charge for xxxxxxx successfully, 2024-03-01-2024-03-31 usage is xxx KWh, charge is xxx CNY. 175 | 2024-06-06 16:01:55 [INFO ] ---- Get month power charge for xxxxxxx successfully, 2024-04-01-2024-04-30 usage is xxx KWh, charge is xxx CNY. 176 | 2024-06-06 16:01:59 [INFO ] ---- Get daily power consumption for xxxxxxx successfully, , 2024-06-05 usage is xxx kwh. 177 | ........ 178 | 2024-12-25 13:43:25 [INFO ] ---- Check the electricity bill balance. When the balance is less than 100.0 CNY, the notification will be sent = True 179 | 2024-12-25 13:43:25 [INFO ] ---- Homeassistant sensor sensor.electricity_charge_balance_xxxx state updated: 102.3 CNY 180 | 2024-12-25 13:43:25 [INFO ] ---- Homeassistant sensor sensor.last_electricity_usage_xxxx state updated: 6.56 kWh 181 | 2024-12-25 13:43:25 [INFO ] ---- Homeassistant sensor sensor.yearly_electricity_usage_xxxx state updated: 1691 kWh 182 | 2024-12-25 13:43:25 [INFO ] ---- Homeassistant sensor sensor.yearly_electricity_charge_xxxx state updated: 758.57 CNY 183 | 2024-12-25 13:43:25 [INFO ] ---- Homeassistant sensor sensor.month_electricity_usage_xxxx state updated: 169 kWh 184 | 2024-12-25 13:43:25 [INFO ] ---- Homeassistant sensor sensor.month_electricity_charge_xxxx state updated: 75.81 CNY 185 | 2024-12-25 13:43:25 [INFO ] ---- User xxxxxxx state-refresh task run successfully! 186 | ``` 187 | 188 | **sensor.electricity_charge_balance_xxxx 为余额传感器** 189 | 190 | 5. 配置configuration.yaml文件, 将下面中的_xxxx 替换为自己log中的_xxxx后缀。 191 | 6. 由于是API方式传递传感器数据,所以要想重启ha实体ID可用,必须配置如下 192 | 193 | ```yaml 194 | template: 195 | - trigger: 196 | - platform: event 197 | event_type: state_changed 198 | event_data: 199 | entity_id: sensor.electricity_charge_balance_xxxx 200 | sensor: 201 | - name: electricity_charge_balance_xxxx 202 | unique_id: electricity_charge_balance_xxxx 203 | state: "{{ states('sensor.electricity_charge_balance_xxxx') }}" 204 | state_class: total 205 | unit_of_measurement: "CNY" 206 | device_class: monetary 207 | 208 | - trigger: 209 | - platform: event 210 | event_type: state_changed 211 | event_data: 212 | entity_id: sensor.last_electricity_usage_xxxx 213 | sensor: 214 | - name: last_electricity_usage_xxxx 215 | unique_id: last_electricity_usage_xxxx 216 | state: "{{ states('sensor.last_electricity_usage_xxxx') }}" 217 | state_class: measurement 218 | unit_of_measurement: "kWh" 219 | device_class: energy 220 | 221 | - trigger: 222 | - platform: event 223 | event_type: state_changed 224 | event_data: 225 | entity_id: sensor.month_electricity_usage_xxxx 226 | sensor: 227 | - name: month_electricity_usage_xxxx 228 | unique_id: month_electricity_usage_xxxx 229 | state: "{{ states('sensor.month_electricity_usage_xxxx') }}" 230 | state_class: measurement 231 | unit_of_measurement: "kWh" 232 | device_class: energy 233 | 234 | - trigger: 235 | - platform: event 236 | event_type: state_changed 237 | event_data: 238 | entity_id: sensor.month_electricity_charge_xxxx 239 | sensor: 240 | - name: month_electricity_charge_xxxx 241 | unique_id: month_electricity_charge_xxxx 242 | state: "{{ states('sensor.month_electricity_charge_xxxx') }}" 243 | state_class: measurement 244 | unit_of_measurement: "CNY" 245 | device_class: monetary 246 | 247 | - trigger: 248 | - platform: event 249 | event_type: state_changed 250 | event_data: 251 | entity_id: sensor.yearly_electricity_usage_xxxx 252 | sensor: 253 | - name: yearly_electricity_usage_xxxx 254 | unique_id: yearly_electricity_usage_xxxx 255 | state: "{{ states('sensor.yearly_electricity_usage_xxxx') }}" 256 | state_class: total_increasing 257 | unit_of_measurement: "kWh" 258 | device_class: energy 259 | 260 | - trigger: 261 | - platform: event 262 | event_type: state_changed 263 | event_data: 264 | entity_id: sensor.yearly_electricity_charge_xxxx 265 | sensor: 266 | - name: yearly_electricity_charge_xxxx 267 | unique_id: yearly_electricity_charge_xxxx 268 | state: "{{ states('sensor.yearly_electricity_charge_xxxx') }}" 269 | state_class: total_increasing 270 | unit_of_measurement: "CNY" 271 | device_class: monetary 272 | ``` 273 | 274 | 配置完成后重启HA, 刷新一下HA界面 275 | 276 | restart.jpg 277 | 278 | 6. 更新容器及其代码(需要更新才需要) 279 | 280 | ```bash 281 | docker-compose down # 删除容器 282 | docker-compose pull # 更新镜像 283 | git pull --tags origin master:master #更新代码,代码不在容器中,所以要手动更新 284 | docker-compose up -d # 重新运行 285 | #如果git 拉取失败可以执行如下命令,重新拉取 286 | git fetch --all 287 | git reset --hard origin/master 288 | git pull 289 | ``` 290 | 291 | ## 4)ha内数据展示 292 | 293 | edit1.jpg 294 | 295 | 结合[mini-graph-card](https://github.com/kalkih/mini-graph-card) 和[mushroom](https://github.com/piitaya/lovelace-mushroom)实现美化效果: 296 | 297 | Ha-mini-card.jpg 298 | 299 | 将下面中的_xxxx 替换为自己log中的_xxxx后缀。 300 | 301 | ```yaml 302 | type: vertical-stack 303 | cards: 304 | - type: custom:mini-graph-card 305 | entities: 306 | - entity: sensor.last_electricity_usage_xxxx 307 | name: 国网每日用电量 308 | aggregate_func: first 309 | show_state: true 310 | show_points: true 311 | icon: mdi:lightning-bolt-outline 312 | - entity: sensor.electricity_charge_balance_xxxx 313 | name: 电费余额 314 | aggregate_func: first 315 | show_state: true 316 | show_points: true 317 | color: "#e74c3c" 318 | icon: mdi:cash 319 | y_axis: secondary 320 | group_by: date 321 | hour24: true 322 | hours_to_show: 240 323 | lower_bound: 0 324 | upper_bound: 10 325 | lower_bound_secondary: 0 326 | upper_bound_secondary: 120 327 | show: 328 | icon: false 329 | - type: horizontal-stack 330 | cards: 331 | - graph: none 332 | type: sensor 333 | entity: sensor.month_electricity_charge_xxxx 334 | detail: 1 335 | name: 上月电费 336 | icon: "" 337 | unit: 元 338 | - graph: none 339 | type: sensor 340 | entity: sensor.month_electricity_usage_xxxx 341 | detail: 1 342 | name: 上月用电量 343 | unit: 度 344 | icon: mdi:lightning-bolt-outline 345 | - type: horizontal-stack 346 | cards: 347 | - animate: true 348 | entities: 349 | - entity: sensor.yearly_electricity_usage_xxxx 350 | name: 今年总用电量 351 | aggregate_func: first 352 | show_state: true 353 | show_points: true 354 | group_by: date 355 | hour24: true 356 | hours_to_show: 240 357 | type: custom:mini-graph-card 358 | - animate: true 359 | entities: 360 | - entity: sensor.yearly_electricity_charge_xxxx 361 | name: 今年总用电费用 362 | aggregate_func: first 363 | show_state: true 364 | show_points: true 365 | group_by: date 366 | hour24: true 367 | hours_to_show: 240 368 | type: custom:mini-graph-card 369 | ``` 370 | 371 | ## 5)电量通知 372 | 373 | 更新电费余额不足提醒,在.env里设置提醒余额。目前我是用[pushplus](https://www.pushplus.plus/)的方案,注册pushplus然后,获取token,通知给谁就让谁注册并将token填到.env中 374 | token获取方法参考[https://cloud.tencent.com/developer/article/2139538](https://cloud.tencent.com/developer/article/2139538) 375 | 376 | # 其他 377 | 378 | > 当前作者:[https://github.com/ARC-MX/sgcc_electricity_new](https://github.com/ARC-MX/sgcc_electricity_new) 379 | > 380 | > 原作者:[https://github.com/louisslee/sgcc_electricity](https://github.com/louisslee/sgcc_electricity),原始[README_origin.md](归档/README_origin.md)。 381 | 382 | ## 我的自定义部分包括: 383 | 384 | 增加的部分: 385 | 386 | - 增加近30天每日电量写入数据库(默认mongodb),其他数据库请自行配置。 387 | - 添加配置默认增加近 7 天每日电量写入数据, 可修改为 30 天, 因为国网目前[「要签约智能交费才能看到30天的数据,不然就只能看到7天的」](https://github.com/ARC-MX/sgcc_electricity_new/issues/11#issuecomment-2158973048)。【注意:开通智能缴费后电费可能从「后付费」变为「预付费」,也就是「欠费即停电」,习惯了每月定时按账单缴费的需要注意,谨防停电风险】 388 | - 将间歇执行设置为定时执行: JOB_START_TIME,24小时制,例如"07:00”则为每天早上7点执行,第一次启动程序立即执行一次, 每12小时执行一次 389 | - 给last_daily_usage增加present_date,用来确定更新的是哪一天的电量。一般查询的日期会晚一到两天。 390 | - 对configuration.yaml中自定义实体部分修改。 391 | 392 | ## 重要修改通知 393 | 394 | 2024-06-13:SQLite替换MongoDB,原因是python自带SQLite3,不需要额外安装,也不再需要MongoDB镜像。 395 | 2024-07-03:新增每天定时执行两次,添加配置默认增加近 7 天每日电量写入数据, 可修改为 30 天。 396 | 2024-07-05:新增余额不足提醒功能。 397 | 2024-12-10:新增忽略指定用户ID的功能:针对一些用户拥有充电或者发电账户,可以使用 IGNORE_USER_ID 环境变量忽略特定的ID。 398 | 2025-01-05:新增Homeassistant Add-on部署方式。 399 | TO-DO 400 | 401 | - [X] 增加离线滑动验证码识别方案 402 | - [X] 添加默认推送服务,电费余额不足提醒 403 | - [X] 添加Homeassistant Add-on安装方式,在此感谢[Ami8834671](https://github.com/Ami8834671), [DuanXDong](https://github.com/DuanXDong)等小伙伴的idea和贡献 404 | - [ ] 添加置Homeassistant integration 405 | 406 | ## **技术交流群** 407 | 408 | 由于现在用户越来越多,稍有问题大家就在github上发issue,我有点回复不过来了,故创建一个付费加入的QQ群。该群只是方便大家讨论,不承诺技术协助,我想大多数用户参考历史issue和文档都能解决自己的问题 409 | 410 | ### 入群方式 411 | 412 | 通过为项目点star并微信打赏备注QQ名或QQ号等信息,入群会审核这些信息 413 | 414 | 415 | 416 | ### 再次说明,希望大家通过认真看文档和浏览历史issue解决问题,毕竟收费群不是开源项目的本意。 417 | -------------------------------------------------------------------------------- /assets/Alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/Alipay.png -------------------------------------------------------------------------------- /assets/Ha-mini-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/Ha-mini-card.jpg -------------------------------------------------------------------------------- /assets/QQ_group.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/QQ_group.jpg -------------------------------------------------------------------------------- /assets/WeiChat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/WeiChat.jpg -------------------------------------------------------------------------------- /assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/background.png -------------------------------------------------------------------------------- /assets/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | Docker Image CI - passing 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Docker Image CI 21 | 22 | 23 | 24 | 25 | 28 | 29 | passing 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /assets/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/database.png -------------------------------------------------------------------------------- /assets/edit1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/edit1.jpg -------------------------------------------------------------------------------- /assets/image-20230730135540291.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/image-20230730135540291.png -------------------------------------------------------------------------------- /assets/image-20240514.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/image-20240514.jpg -------------------------------------------------------------------------------- /assets/restart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/assets/restart.jpg -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | name: "SGCC Electricity" 2 | version: "v1.6.6" 3 | slug: "sgcc_electricity" 4 | description: "获取国网电费数据的插件" 5 | url: "https://github.com/ARC-MX/sgcc_electricity_new" 6 | arch: 7 | - aarch64 8 | - amd64 9 | host_network: true 10 | startup: application 11 | boot: auto 12 | init: false 13 | image: "registry.cn-hangzhou.aliyuncs.com/arcw/sgcc_electricity-{arch}" 14 | map: 15 | - config:rw 16 | options: 17 | PHONE_NUMBER: "" 18 | PASSWORD: "" 19 | IGNORE_USER_ID: "xxxx,xxxx" 20 | ENABLE_DATABASE_STORAGE: false 21 | DB_NAME: "homeassistant.db" 22 | HASS_URL: "http://homeassistant.local:8123/" 23 | HASS_TOKEN: "" 24 | JOB_START_TIME: "07:00" 25 | RETRY_WAIT_TIME_OFFSET_UNIT: 15 26 | DATA_RETENTION_DAYS: 7 27 | RECHARGE_NOTIFY: false 28 | BALANCE: 5.0 29 | PUSHPLUS_TOKEN: "xxxx,xxxx" 30 | schema: 31 | PHONE_NUMBER: str 32 | PASSWORD: password 33 | IGNORE_USER_ID: str 34 | ENABLE_DATABASE_STORAGE: bool 35 | DB_NAME: str 36 | HASS_URL: url 37 | HASS_TOKEN: str 38 | JOB_START_TIME: str 39 | RETRY_WAIT_TIME_OFFSET_UNIT: int(2,30) 40 | DATA_RETENTION_DAYS: int 41 | RECHARGE_NOTIFY: bool 42 | BALANCE: float 43 | PUSHPLUS_TOKEN: str 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sgcc_electricity_app: 3 | env_file: 4 | - .env 5 | image: registry.cn-hangzhou.aliyuncs.com/arcw/sgcc_electricity:latest # for use docker.io: arcw/sgcc_electricity:latest 6 | container_name: sgcc_electricity 7 | network_mode: "host" 8 | environment: 9 | - SET_CONTAINER_TIMEZONE=true 10 | - CONTAINER_TIMEZONE=Asia/Shanghai 11 | restart: unless-stopped 12 | volumes: 13 | - ./:/data # if you want to read homeassistant.db, homeassistant.db is in the container at /data/ 14 | command: python3 main.py 15 | init: true 16 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | ### 以下项都需要修改 2 | ## 国网登录信息 3 | # 修改为自己的登录账号 4 | PHONE_NUMBER="xxx" 5 | # 修改为自己的登录密码 6 | PASSWORD="xxxx" 7 | # 排除指定用户ID,如果出现一些不想检测的ID或者有些充电、发电帐号、可以使用这个环境变量,如果有多个就用","分隔,","之间不要有空格 8 | IGNORE_USER_ID=xxxxxxx,xxxxxxx,xxxxxxx 9 | 10 | # SQLite 数据库配置 11 | # or False 不启用数据库储存每日用电量数据。 12 | ENABLE_DATABASE_STORAGE=False 13 | # 数据库名,默认为homeassistant 14 | DB_NAME="homeassistant.db" 15 | 16 | ## homeassistant配置 17 | # 改为你的localhost为你的homeassistant地址 18 | HASS_URL="http://localhost:8123/" 19 | # homeassistant的长期令牌 20 | HASS_TOKEN="eyxxxxx" 21 | 22 | ## selenium运行参数 23 | # 任务开始时间,24小时制,例如"07:00”则为每天早上7点执行,第一次启动程序如果时间晚于早上7点则会立即执行一次,每隔12小时执行一次。 24 | JOB_START_TIME="07:00" 25 | # 每次操作等待时间,推荐设定范围为[2,30],该值表示每次点击网页后所要等待数据加载的时间,如果出现“no such element”诸如此类的错误可适当调大该值,如果硬件性能较好可以适当调小该值 26 | RETRY_WAIT_TIME_OFFSET_UNIT=15 27 | 28 | 29 | ## 记录的天数, 仅支持填写 7 或 30 30 | # 国网原本可以记录 30 天,现在不开通智能缴费只能查询 7 天造成错误 31 | DATA_RETENTION_DAYS=7 32 | 33 | ## 余额提醒 34 | # 是否缴费提醒 35 | RECHARGE_NOTIFY=False 36 | # 余额 37 | BALANCE=5.0 38 | # pushplus token 如果有多个就用","分隔,","之间不要有空格 39 | PUSHPLUS_TOKEN=xxxxxxx,xxxxxxx,xxxxxxx -------------------------------------------------------------------------------- /ha_addons_doc/Add-on教程.md: -------------------------------------------------------------------------------- 1 | # Home Assistant sgcc electricity new Add-on 安装教程 2 | 3 | ## 安装步骤 4 | 5 | ### 1. 添加add-on存储库 6 | 7 | - 打开 Home Assistant 8 | - 进入设置页面,点击 "Add-ons"或"加载项" 9 | ![进入Add-ons页面](./img/addons-page.png) 10 | - 点击右下角的 "ADD-ON STORE"或"加载项商店" 11 | ![打开Add-on商店](./img/addon-store.png) 12 | - 点击右上角的三个点菜单 13 | - 选择 "Repositories"或"仓库" 14 | ![添加存储库入口](./img/repositories-menu.png) 15 | - 在弹出的对话框中输入sgcc electricity new存储库地址 16 | - 地址:https://github.com/ARC-MX/sgcc_electricity_new 17 | - 推荐使用国内源:[https://gitee.com/ARC-MX/sgcc_electricity_new](https://gitee.com/ARC-MX/sgcc_electricity_new) 18 | - 点击 "ADD" 或 "添加" 确认添加 19 | ![添加存储库地址](./img/add-repository.png) 20 | 21 | ### 2. 安装 Add-on 22 | 23 | - 点击右上角的三个点菜单 24 | - 选择 "Refresh" 或 "检查更新" 25 | ![检查更新](./img/refresh.png) 26 | - 在列表中找到新添加的第三方 Add-on 27 | ![查找Add-on](./img/find-addon.png) 28 | - 点击想要安装的 Add-on 29 | - 点击 "INSTALL" 或 "安装" 开始安装 30 | ![安装Add-on](./img/install-addon.png) 31 | - 等待安装完成 32 | ![安装完成](./img/installation-complete.png) 33 | 34 | ### 3. 配置和启动 35 | 36 | - 安装完成后,点击 "CONFIGURATION" 或 "配置" 标签 37 | - 根据需要修改配置参数 38 | ![配置Add-on](./img/configuration.png) 39 | - 点击显示未使用的可选配置选项按钮,可以配置ignore_user_id等可选参数 40 | ![显示未使用的可选配置选项](./img/show-unused-options.png)![配置ignore_user_id](./img/configuration-ignore_user_id.png) 41 | - 点击 "SAVE" 或 "保存" 保存配置 42 | - 返回 "Info" 或 "信息" 标签页 43 | - 点击 "START" 或 "启动" 启动 Add-on 44 | ![启动Add-on](./img/start-addon.png) 45 | - 启动后,点击 "日志" 标签页,可以看到Add-on的运行状态 46 | ![Add-on运行状态](./img/addon-running-status.png) 47 | 48 | ## 常见问题 49 | 50 | - 如果无法找到新添加的 Add-on,请尝试刷新页面 51 | - 如果安装失败,检查存储库地址是否正确 52 | - 遇到问题可以查看 "日志" 标签页的日志信息 53 | -------------------------------------------------------------------------------- /ha_addons_doc/img/add-repository.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/add-repository.png -------------------------------------------------------------------------------- /ha_addons_doc/img/addon-running-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/addon-running-status.png -------------------------------------------------------------------------------- /ha_addons_doc/img/addon-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/addon-store.png -------------------------------------------------------------------------------- /ha_addons_doc/img/addons-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/addons-page.png -------------------------------------------------------------------------------- /ha_addons_doc/img/configuration-ignore_user_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/configuration-ignore_user_id.png -------------------------------------------------------------------------------- /ha_addons_doc/img/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/configuration.png -------------------------------------------------------------------------------- /ha_addons_doc/img/find-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/find-addon.png -------------------------------------------------------------------------------- /ha_addons_doc/img/install-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/install-addon.png -------------------------------------------------------------------------------- /ha_addons_doc/img/installation-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/installation-complete.png -------------------------------------------------------------------------------- /ha_addons_doc/img/installation-progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/installation-progress.png -------------------------------------------------------------------------------- /ha_addons_doc/img/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/refresh.png -------------------------------------------------------------------------------- /ha_addons_doc/img/repositories-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/repositories-menu.png -------------------------------------------------------------------------------- /ha_addons_doc/img/show-unused-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/show-unused-options.png -------------------------------------------------------------------------------- /ha_addons_doc/img/start-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/ha_addons_doc/img/start-addon.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/icon.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/logo.png -------------------------------------------------------------------------------- /repository.yaml: -------------------------------------------------------------------------------- 1 | name: sgcc_electricity add-on repository 2 | url: 'https://github.com/ARC-MX/sgcc_electricity_new' 3 | maintainer: sgcc_electricity 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.31.0 2 | selenium==4.19.0 3 | schedule==1.2.1 4 | Pillow==10.1.0 5 | undetected_chromedriver==3.5.4 6 | onnxruntime==1.18.1 7 | numpy==1.26.2 8 | # python-dotenv 9 | # python-dateutil -------------------------------------------------------------------------------- /scripts/captcha.onnx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARC-MX/sgcc_electricity_new/aa69ff7dfc50dc6413304f294a03b50254f0482a/scripts/captcha.onnx -------------------------------------------------------------------------------- /scripts/const.py: -------------------------------------------------------------------------------- 1 | # 填写普通参数 不要填写密码等敏感信息 2 | # 国网电力官网 3 | LOGIN_URL = "https://www.95598.cn/osgweb/login" 4 | ELECTRIC_USAGE_URL = "https://www.95598.cn/osgweb/electricityCharge" 5 | BALANCE_URL = "https://www.95598.cn/osgweb/userAcc" 6 | 7 | 8 | # Home Assistant 9 | SUPERVISOR_URL = "http://supervisor/core" 10 | API_PATH = "/api/states/" # https://developers.home-assistant.io/docs/api/rest/ 11 | 12 | BALANCE_SENSOR_NAME = "sensor.electricity_charge_balance" 13 | DAILY_USAGE_SENSOR_NAME = "sensor.last_electricity_usage" 14 | YEARLY_USAGE_SENSOR_NAME = "sensor.yearly_electricity_usage" 15 | YEARLY_CHARGE_SENSOR_NAME = "sensor.yearly_electricity_charge" 16 | MONTH_USAGE_SENSOR_NAME = "sensor.month_electricity_usage" 17 | MONTH_CHARGE_SENSOR_NAME = "sensor.month_electricity_charge" 18 | BALANCE_UNIT = "CNY" 19 | USAGE_UNIT = "KWH" 20 | 21 | -------------------------------------------------------------------------------- /scripts/data_fetcher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | import subprocess 5 | import time 6 | 7 | import random 8 | import base64 9 | import sqlite3 10 | import undetected_chromedriver as uc 11 | from datetime import datetime 12 | from selenium import webdriver 13 | from selenium.webdriver import ActionChains 14 | from selenium.webdriver.chrome.options import Options 15 | from selenium.webdriver.common.by import By 16 | from selenium.webdriver.support import expected_conditions as EC 17 | from selenium.webdriver.support.wait import WebDriverWait 18 | from sensor_updator import SensorUpdator 19 | 20 | from const import * 21 | 22 | import numpy as np 23 | # import cv2 24 | from io import BytesIO 25 | from PIL import Image 26 | from onnx import ONNX 27 | import platform 28 | 29 | 30 | def base64_to_PLI(base64_str: str): 31 | base64_data = re.sub('^data:image/.+;base64,', '', base64_str) 32 | byte_data = base64.b64decode(base64_data) 33 | image_data = BytesIO(byte_data) 34 | img = Image.open(image_data) 35 | return img 36 | 37 | def get_transparency_location(image): 38 | '''获取基于透明元素裁切图片的左上角、右下角坐标 39 | 40 | :param image: cv2加载好的图像 41 | :return: (left, upper, right, lower)元组 42 | ''' 43 | # 1. 扫描获得最左边透明点和最右边透明点坐标 44 | height, width, channel = image.shape # 高、宽、通道数 45 | assert channel == 4 # 无透明通道报错 46 | first_location = None # 最先遇到的透明点 47 | last_location = None # 最后遇到的透明点 48 | first_transparency = [] # 从左往右最先遇到的透明点,元素个数小于等于图像高度 49 | last_transparency = [] # 从左往右最后遇到的透明点,元素个数小于等于图像高度 50 | for y, rows in enumerate(image): 51 | for x, BGRA in enumerate(rows): 52 | alpha = BGRA[3] 53 | if alpha != 0: 54 | if not first_location or first_location[1] != y: # 透明点未赋值或为同一列 55 | first_location = (x, y) # 更新最先遇到的透明点 56 | first_transparency.append(first_location) 57 | last_location = (x, y) # 更新最后遇到的透明点 58 | if last_location: 59 | last_transparency.append(last_location) 60 | 61 | # 2. 矩形四个边的中点 62 | top = first_transparency[0] 63 | bottom = first_transparency[-1] 64 | left = None 65 | right = None 66 | for first, last in zip(first_transparency, last_transparency): 67 | if not left: 68 | left = first 69 | if not right: 70 | right = last 71 | if first[0] < left[0]: 72 | left = first 73 | if last[0] > right[0]: 74 | right = last 75 | 76 | # 3. 左上角、右下角 77 | upper_left = (left[0], top[1]) # 左上角 78 | bottom_right = (right[0], bottom[1]) # 右下角 79 | 80 | return upper_left[0], upper_left[1], bottom_right[0], bottom_right[1] 81 | 82 | class DataFetcher: 83 | 84 | def __init__(self, username: str, password: str): 85 | if 'PYTHON_IN_DOCKER' not in os.environ: 86 | import dotenv 87 | dotenv.load_dotenv(verbose=True) 88 | self._username = username 89 | self._password = password 90 | self.onnx = ONNX("./captcha.onnx") 91 | if platform.system() == 'Windows': 92 | pass 93 | else: 94 | self._chromium_version = self._get_chromium_version() 95 | 96 | # 获取 ENABLE_DATABASE_STORAGE 的值,默认为 False 97 | self.enable_database_storage = os.getenv("ENABLE_DATABASE_STORAGE", "false").lower() == "true" 98 | self.DRIVER_IMPLICITY_WAIT_TIME = int(os.getenv("DRIVER_IMPLICITY_WAIT_TIME", 60)) 99 | self.RETRY_TIMES_LIMIT = int(os.getenv("RETRY_TIMES_LIMIT", 5)) 100 | self.LOGIN_EXPECTED_TIME = int(os.getenv("LOGIN_EXPECTED_TIME", 10)) 101 | self.RETRY_WAIT_TIME_OFFSET_UNIT = int(os.getenv("RETRY_WAIT_TIME_OFFSET_UNIT", 10)) 102 | self.IGNORE_USER_ID = os.getenv("IGNORE_USER_ID", "xxxxx,xxxxx").split(",") 103 | 104 | # @staticmethod 105 | def _click_button(self, driver, button_search_type, button_search_key): 106 | '''wrapped click function, click only when the element is clickable''' 107 | click_element = driver.find_element(button_search_type, button_search_key) 108 | # logging.info(f"click_element:{button_search_key}.is_displayed() = {click_element.is_displayed()}\r") 109 | # logging.info(f"click_element:{button_search_key}.is_enabled() = {click_element.is_enabled()}\r") 110 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.element_to_be_clickable(click_element)) 111 | driver.execute_script("arguments[0].click();", click_element) 112 | 113 | # @staticmethod 114 | def _is_captcha_legal(self, captcha): 115 | ''' check the ddddocr result, justify whether it's legal''' 116 | if (len(captcha) != 4): 117 | return False 118 | for s in captcha: 119 | if (not s.isalpha() and not s.isdigit()): 120 | return False 121 | return True 122 | 123 | # @staticmethod 124 | def _get_chromium_version(self): 125 | result = str(subprocess.check_output(["chromium", "--product-version"])) 126 | version = re.findall(r"(\d*)\.", result)[0] 127 | logging.info(f"chromium-driver version is {version}") 128 | return int(version) 129 | 130 | # @staticmethod 131 | def _sliding_track(self, driver, distance):# 机器模拟人工滑动轨迹 132 | # 获取按钮 133 | slider = driver.find_element(By.CLASS_NAME, "slide-verify-slider-mask-item") 134 | ActionChains(driver).click_and_hold(slider).perform() 135 | # 获取轨迹 136 | # tracks = _get_tracks(distance) 137 | # for t in tracks: 138 | yoffset_random = random.uniform(-2, 4) 139 | ActionChains(driver).move_by_offset(xoffset=distance, yoffset=yoffset_random).perform() 140 | # time.sleep(0.2) 141 | ActionChains(driver).release().perform() 142 | 143 | def connect_user_db(self, user_id): 144 | """创建数据库集合,db_name = electricity_daily_usage_{user_id} 145 | :param user_id: 用户ID""" 146 | try: 147 | # 创建数据库 148 | DB_NAME = os.getenv("DB_NAME", "homeassistant.db") 149 | if 'PYTHON_IN_DOCKER' in os.environ: 150 | DB_NAME = "/data/" + DB_NAME 151 | self.connect = sqlite3.connect(DB_NAME) 152 | self.connect.cursor() 153 | logging.info(f"Database of {DB_NAME} created successfully.") 154 | # 创建表名 155 | self.table_name = f"daily{user_id}" 156 | sql = f'''CREATE TABLE IF NOT EXISTS {self.table_name} ( 157 | date DATE PRIMARY KEY NOT NULL, 158 | usage REAL NOT NULL)''' 159 | self.connect.execute(sql) 160 | logging.info(f"Table {self.table_name} created successfully") 161 | 162 | # 创建data表名 163 | self.table_expand_name = f"data{user_id}" 164 | sql = f'''CREATE TABLE IF NOT EXISTS {self.table_expand_name} ( 165 | name TEXT PRIMARY KEY NOT NULL, 166 | value TEXT NOT NULL)''' 167 | self.connect.execute(sql) 168 | logging.info(f"Table {self.table_expand_name} created successfully") 169 | 170 | # 如果表已存在,则不会创建 171 | except sqlite3.Error as e: 172 | logging.debug(f"Create db or Table error:{e}") 173 | return False 174 | return True 175 | 176 | def insert_data(self, data:dict): 177 | if self.connect is None: 178 | logging.error("Database connection is not established.") 179 | return 180 | # 创建索引 181 | try: 182 | sql = f"INSERT OR REPLACE INTO {self.table_name} VALUES(strftime('%Y-%m-%d','{data['date']}'),{data['usage']});" 183 | self.connect.execute(sql) 184 | self.connect.commit() 185 | except BaseException as e: 186 | logging.debug(f"Data update failed: {e}") 187 | 188 | def insert_expand_data(self, data:dict): 189 | if self.connect is None: 190 | logging.error("Database connection is not established.") 191 | return 192 | # 创建索引 193 | try: 194 | sql = f"INSERT OR REPLACE INTO {self.table_expand_name} VALUES('{data['name']}','{data['value']}');" 195 | self.connect.execute(sql) 196 | self.connect.commit() 197 | except BaseException as e: 198 | logging.debug(f"Data update failed: {e}") 199 | 200 | 201 | def _get_webdriver(self): 202 | chrome_options = Options() 203 | chrome_options.add_argument('--incognito') 204 | chrome_options.add_argument('--window-size=4000,1600') 205 | chrome_options.add_argument('--headless') 206 | chrome_options.add_argument('--no-sandbox') 207 | chrome_options.add_argument('--disable-gpu') 208 | chrome_options.add_argument('--disable-dev-shm-usage') 209 | driver = uc.Chrome(driver_executable_path="/usr/bin/chromedriver", options=chrome_options, version_main=self._chromium_version) 210 | driver.implicitly_wait(self.DRIVER_IMPLICITY_WAIT_TIME) 211 | return driver 212 | 213 | def _login(self, driver, phone_code = False): 214 | 215 | driver.get(LOGIN_URL) 216 | logging.info(f"Open LOGIN_URL:{LOGIN_URL}.\r") 217 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 218 | # swtich to username-password login page 219 | driver.find_element(By.CLASS_NAME, "user").click() 220 | logging.info("find_element 'user'.\r") 221 | self._click_button(driver, By.XPATH, '//*[@id="login_box"]/div[1]/div[1]/div[2]/span') 222 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 223 | # click agree button 224 | self._click_button(driver, By.XPATH, '//*[@id="login_box"]/div[2]/div[1]/form/div[1]/div[3]/div/span[2]') 225 | logging.info("Click the Agree option.\r") 226 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 227 | if phone_code: 228 | self._click_button(driver, By.XPATH, '//*[@id="login_box"]/div[1]/div[1]/div[3]/span') 229 | input_elements = driver.find_elements(By.CLASS_NAME, "el-input__inner") 230 | input_elements[2].send_keys(self._username) 231 | logging.info(f"input_elements username : {self._username}\r") 232 | self._click_button(driver, By.XPATH, '//*[@id="login_box"]/div[2]/div[2]/form/div[1]/div[2]/div[2]/div/a') 233 | code = input("Input your phone verification code: ") 234 | input_elements[3].send_keys(code) 235 | logging.info(f"input_elements verification code: {code}.\r") 236 | # click login button 237 | self._click_button(driver, By.XPATH, '//*[@id="login_box"]/div[2]/div[2]/form/div[2]/div/button/span') 238 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT*2) 239 | logging.info("Click login button.\r") 240 | 241 | return True 242 | else : 243 | # input username and password 244 | input_elements = driver.find_elements(By.CLASS_NAME, "el-input__inner") 245 | input_elements[0].send_keys(self._username) 246 | logging.info(f"input_elements username : {self._username}\r") 247 | input_elements[1].send_keys(self._password) 248 | logging.info(f"input_elements password : {self._password}\r") 249 | 250 | # click login button 251 | self._click_button(driver, By.CLASS_NAME, "el-button.el-button--primary") 252 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT*2) 253 | logging.info("Click login button.\r") 254 | # sometimes ddddOCR may fail, so add retry logic) 255 | for retry_times in range(1, self.RETRY_TIMES_LIMIT + 1): 256 | 257 | self._click_button(driver, By.XPATH, '//*[@id="login_box"]/div[1]/div[1]/div[2]/span') 258 | #get canvas image 259 | background_JS = 'return document.getElementById("slideVerify").childNodes[0].toDataURL("image/png");' 260 | # targe_JS = 'return document.getElementsByClassName("slide-verify-block")[0].toDataURL("image/png");' 261 | # get base64 image data 262 | im_info = driver.execute_script(background_JS) 263 | background = im_info.split(',')[1] 264 | background_image = base64_to_PLI(background) 265 | logging.info(f"Get electricity canvas image successfully.\r") 266 | distance = self.onnx.get_distance(background_image) 267 | logging.info(f"Image CaptCHA distance is {distance}.\r") 268 | 269 | self._sliding_track(driver, round(distance*1.06)) #1.06是补偿 270 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 271 | if (driver.current_url == LOGIN_URL): # if login not success 272 | try: 273 | logging.info(f"Sliding CAPTCHA recognition failed and reloaded.\r") 274 | self._click_button(driver, By.CLASS_NAME, "el-button.el-button--primary") 275 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT*2) 276 | continue 277 | except: 278 | logging.debug( 279 | f"Login failed, maybe caused by invalid captcha, {self.RETRY_TIMES_LIMIT - retry_times} retry times left.") 280 | else: 281 | return True 282 | logging.error(f"Login failed, maybe caused by Sliding CAPTCHA recognition failed") 283 | return False 284 | 285 | raise Exception( 286 | "Login failed, maybe caused by 1.incorrect phone_number and password, please double check. or 2. network, please mnodify LOGIN_EXPECTED_TIME in .env and run docker compose up --build.") 287 | 288 | def fetch(self): 289 | 290 | """main logic here""" 291 | if platform.system() == 'Windows': 292 | driverfile_path = r'C:\Users\mxwang\Project\msedgedriver.exe' 293 | driver = webdriver.Edge(executable_path=driverfile_path) 294 | else: 295 | driver = self._get_webdriver() 296 | 297 | driver.maximize_window() 298 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 299 | logging.info("Webdriver initialized.") 300 | updator = SensorUpdator() 301 | 302 | try: 303 | if os.getenv("DEBUG_MODE", "false").lower() == "true": 304 | if self._login(driver,phone_code=True): 305 | logging.info("login successed !") 306 | else: 307 | logging.info("login unsuccessed !") 308 | raise Exception("login unsuccessed") 309 | else: 310 | if self._login(driver): 311 | logging.info("login successed !") 312 | else: 313 | logging.info("login unsuccessed !") 314 | raise Exception("login unsuccessed") 315 | except Exception as e: 316 | logging.error( 317 | f"Webdriver quit abnormly, reason: {e}. {self.RETRY_TIMES_LIMIT} retry times left.") 318 | driver.quit() 319 | return 320 | 321 | logging.info(f"Login successfully on {LOGIN_URL}") 322 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 323 | logging.info(f"Try to get the userid list") 324 | user_id_list = self._get_user_ids(driver) 325 | logging.info(f"Here are a total of {len(user_id_list)} userids, which are {user_id_list} among which {self.IGNORE_USER_ID} will be ignored.") 326 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 327 | 328 | 329 | for userid_index, user_id in enumerate(user_id_list): 330 | try: 331 | # switch to electricity charge balance page 332 | driver.get(BALANCE_URL) 333 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 334 | self._choose_current_userid(driver,userid_index) 335 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 336 | current_userid = self._get_current_userid(driver) 337 | if current_userid in self.IGNORE_USER_ID: 338 | logging.info(f"The user ID {current_userid} will be ignored in user_id_list") 339 | continue 340 | else: 341 | ### get data 342 | balance, last_daily_date, last_daily_usage, yearly_charge, yearly_usage, month_charge, month_usage = self._get_all_data(driver, user_id, userid_index) 343 | updator.update_one_userid(user_id, balance, last_daily_date, last_daily_usage, yearly_charge, yearly_usage, month_charge, month_usage) 344 | 345 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 346 | except Exception as e: 347 | if (userid_index != len(user_id_list)): 348 | logging.info(f"The current user {user_id} data fetching failed {e}, the next user data will be fetched.") 349 | else: 350 | logging.info(f"The user {user_id} data fetching failed, {e}") 351 | logging.info("Webdriver quit after fetching data successfully.") 352 | continue 353 | 354 | driver.quit() 355 | 356 | 357 | def _get_current_userid(self, driver): 358 | current_userid = driver.find_element(By.XPATH, '//*[@id="app"]/div/div/article/div/div/div[2]/div/div/div[1]/div[2]/div/div/div/div[2]/div/div[1]/div/ul/div/li[1]/span[2]').text 359 | return current_userid 360 | 361 | def _choose_current_userid(self, driver, userid_index): 362 | elements = driver.find_elements(By.CLASS_NAME, "button_confirm") 363 | if elements: 364 | self._click_button(driver, By.XPATH, f'''//*[@id="app"]/div/div[2]/div/div/div/div[2]/div[2]/div/button''') 365 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 366 | self._click_button(driver, By.CLASS_NAME, "el-input__suffix") 367 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 368 | self._click_button(driver, By.XPATH, f"/html/body/div[2]/div[1]/div[1]/ul/li[{userid_index+1}]/span") 369 | 370 | 371 | def _get_all_data(self, driver, user_id, userid_index): 372 | balance = self._get_electric_balance(driver) 373 | if (balance is None): 374 | logging.info(f"Get electricity charge balance for {user_id} failed, Pass.") 375 | else: 376 | logging.info( 377 | f"Get electricity charge balance for {user_id} successfully, balance is {balance} CNY.") 378 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 379 | # swithc to electricity usage page 380 | driver.get(ELECTRIC_USAGE_URL) 381 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 382 | self._choose_current_userid(driver, userid_index) 383 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 384 | # get data for each user id 385 | yearly_usage, yearly_charge = self._get_yearly_data(driver) 386 | 387 | if yearly_usage is None: 388 | logging.error(f"Get year power usage for {user_id} failed, pass") 389 | else: 390 | logging.info( 391 | f"Get year power usage for {user_id} successfully, usage is {yearly_usage} kwh") 392 | if yearly_charge is None: 393 | logging.error(f"Get year power charge for {user_id} failed, pass") 394 | else: 395 | logging.info( 396 | f"Get year power charge for {user_id} successfully, yealrly charge is {yearly_charge} CNY") 397 | 398 | # 按月获取数据 399 | month, month_usage, month_charge = self._get_month_usage(driver) 400 | if month is None: 401 | logging.error(f"Get month power usage for {user_id} failed, pass") 402 | else: 403 | for m in range(len(month)): 404 | logging.info(f"Get month power charge for {user_id} successfully, {month[m]} usage is {month_usage[m]} KWh, charge is {month_charge[m]} CNY.") 405 | # get yesterday usage 406 | last_daily_date, last_daily_usage = self._get_yesterday_usage(driver) 407 | if last_daily_usage is None: 408 | logging.error(f"Get daily power consumption for {user_id} failed, pass") 409 | else: 410 | logging.info( 411 | f"Get daily power consumption for {user_id} successfully, , {last_daily_date} usage is {last_daily_usage} kwh.") 412 | if month is None: 413 | logging.error(f"Get month power usage for {user_id} failed, pass") 414 | 415 | # 新增储存用电量 416 | if self.enable_database_storage: 417 | # 将数据存储到数据库 418 | logging.info("enable_database_storage is true, we will store the data to the database.") 419 | # 按天获取数据 7天/30天 420 | date, usages = self._get_daily_usage_data(driver) 421 | self._save_user_data(user_id, balance, last_daily_date, last_daily_usage, date, usages, month, month_usage, month_charge, yearly_charge, yearly_usage) 422 | else: 423 | logging.info("enable_database_storage is false, we will not store the data to the database.") 424 | 425 | 426 | if month_charge: 427 | month_charge = month_charge[-1] 428 | else: 429 | month_charge = None 430 | if month_usage: 431 | month_usage = month_usage[-1] 432 | else: 433 | month_usage = None 434 | 435 | return balance, last_daily_date, last_daily_usage, yearly_charge, yearly_usage, month_charge, month_usage 436 | 437 | def _get_user_ids(self, driver): 438 | try: 439 | # 刷新网页 440 | driver.refresh() 441 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT*2) 442 | element = WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.presence_of_element_located((By.CLASS_NAME, 'el-dropdown'))) 443 | # click roll down button for user id 444 | self._click_button(driver, By.XPATH, "//div[@class='el-dropdown']/span") 445 | logging.debug(f'''self._click_button(driver, By.XPATH, "//div[@class='el-dropdown']/span")''') 446 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 447 | # wait for roll down menu displayed 448 | target = driver.find_element(By.CLASS_NAME, "el-dropdown-menu.el-popper").find_element(By.TAG_NAME, "li") 449 | logging.debug(f'''target = driver.find_element(By.CLASS_NAME, "el-dropdown-menu.el-popper").find_element(By.TAG_NAME, "li")''') 450 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 451 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.visibility_of(target)) 452 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 453 | logging.debug(f'''WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.visibility_of(target))''') 454 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until( 455 | EC.text_to_be_present_in_element((By.XPATH, "//ul[@class='el-dropdown-menu el-popper']/li"), ":")) 456 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 457 | 458 | # get user id one by one 459 | userid_elements = driver.find_element(By.CLASS_NAME, "el-dropdown-menu.el-popper").find_elements(By.TAG_NAME, "li") 460 | userid_list = [] 461 | for element in userid_elements: 462 | userid_list.append(re.findall("[0-9]+", element.text)[-1]) 463 | return userid_list 464 | except Exception as e: 465 | logging.error( 466 | f"Webdriver quit abnormly, reason: {e}. get user_id list failed.") 467 | driver.quit() 468 | 469 | def _get_electric_balance(self, driver): 470 | try: 471 | balance = driver.find_element(By.CLASS_NAME, "num").text 472 | balance_text = driver.find_element(By.CLASS_NAME, "amttxt").text 473 | if "欠费" in balance_text : 474 | return -float(balance) 475 | else: 476 | return float(balance) 477 | except: 478 | return None 479 | 480 | def _get_yearly_data(self, driver): 481 | 482 | try: 483 | if datetime.now().month == 1: 484 | self._click_button(driver, By.XPATH, '//*[@id="pane-first"]/div[1]/div/div[1]/div/div/input') 485 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 486 | span_element = driver.find_element(By.XPATH, f"//span[contains(text(), '{datetime.now().year - 1}')]") 487 | span_element.click() 488 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 489 | self._click_button(driver, By.XPATH, "//div[@class='el-tabs__nav is-top']/div[@id='tab-first']") 490 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 491 | # wait for data displayed 492 | target = driver.find_element(By.CLASS_NAME, "total") 493 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.visibility_of(target)) 494 | except Exception as e: 495 | logging.error(f"The yearly data get failed : {e}") 496 | return None, None 497 | 498 | # get data 499 | try: 500 | yearly_usage = driver.find_element(By.XPATH, "//ul[@class='total']/li[1]/span").text 501 | except Exception as e: 502 | logging.error(f"The yearly_usage data get failed : {e}") 503 | yearly_usage = None 504 | 505 | try: 506 | yearly_charge = driver.find_element(By.XPATH, "//ul[@class='total']/li[2]/span").text 507 | except Exception as e: 508 | logging.error(f"The yearly_charge data get failed : {e}") 509 | yearly_charge = None 510 | 511 | return yearly_usage, yearly_charge 512 | 513 | def _get_yesterday_usage(self, driver): 514 | """获取最近一次用电量""" 515 | try: 516 | # 点击日用电量 517 | self._click_button(driver, By.XPATH, "//div[@class='el-tabs__nav is-top']/div[@id='tab-second']") 518 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 519 | # wait for data displayed 520 | usage_element = driver.find_element(By.XPATH, 521 | "//div[@class='el-tab-pane dayd']//div[@class='el-table__body-wrapper is-scrolling-none']/table/tbody/tr[1]/td[2]/div") 522 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.visibility_of(usage_element)) # 等待用电量出现 523 | 524 | # 增加是哪一天 525 | date_element = driver.find_element(By.XPATH, 526 | "//div[@class='el-tab-pane dayd']//div[@class='el-table__body-wrapper is-scrolling-none']/table/tbody/tr[1]/td[1]/div") 527 | last_daily_date = date_element.text # 获取最近一次用电量的日期 528 | return last_daily_date, float(usage_element.text) 529 | except Exception as e: 530 | logging.error(f"The yesterday data get failed : {e}") 531 | return None 532 | 533 | def _get_month_usage(self, driver): 534 | """获取每月用电量""" 535 | 536 | try: 537 | self._click_button(driver, By.XPATH, "//div[@class='el-tabs__nav is-top']/div[@id='tab-first']") 538 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 539 | if datetime.now().month == 1: 540 | self._click_button(driver, By.XPATH, '//*[@id="pane-first"]/div[1]/div/div[1]/div/div/input') 541 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 542 | span_element = driver.find_element(By.XPATH, f"//span[contains(text(), '{datetime.now().year - 1}')]") 543 | span_element.click() 544 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 545 | # wait for month displayed 546 | target = driver.find_element(By.CLASS_NAME, "total") 547 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.visibility_of(target)) 548 | month_element = driver.find_element(By.XPATH, "//*[@id='pane-first']/div[1]/div[2]/div[2]/div/div[3]/table/tbody").text 549 | month_element = month_element.split("\n") 550 | month_element.remove("MAX") 551 | month_element = np.array(month_element).reshape(-1, 3) 552 | # 将每月的用电量保存为List 553 | month = [] 554 | usage = [] 555 | charge = [] 556 | for i in range(len(month_element)): 557 | month.append(month_element[i][0]) 558 | usage.append(month_element[i][1]) 559 | charge.append(month_element[i][2]) 560 | return month, usage, charge 561 | except Exception as e: 562 | logging.error(f"The month data get failed : {e}") 563 | return None,None,None 564 | 565 | # 增加获取每日用电量的函数 566 | def _get_daily_usage_data(self, driver): 567 | """储存指定天数的用电量""" 568 | retention_days = int(os.getenv("DATA_RETENTION_DAYS", 7)) # 默认值为7天 569 | self._click_button(driver, By.XPATH, "//div[@class='el-tabs__nav is-top']/div[@id='tab-second']") 570 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 571 | 572 | # 7 天在第一个 label, 30 天 开通了智能缴费之后才会出现在第二个, (sb sgcc) 573 | if retention_days == 7: 574 | self._click_button(driver, By.XPATH, "//*[@id='pane-second']/div[1]/div/label[1]/span[1]") 575 | elif retention_days == 30: 576 | self._click_button(driver, By.XPATH, "//*[@id='pane-second']/div[1]/div/label[2]/span[1]") 577 | else: 578 | logging.error(f"Unsupported retention days value: {retention_days}") 579 | return 580 | 581 | time.sleep(self.RETRY_WAIT_TIME_OFFSET_UNIT) 582 | 583 | # 等待用电量的数据出现 584 | usage_element = driver.find_element(By.XPATH, 585 | "//div[@class='el-tab-pane dayd']//div[@class='el-table__body-wrapper is-scrolling-none']/table/tbody/tr[1]/td[2]/div") 586 | WebDriverWait(driver, self.DRIVER_IMPLICITY_WAIT_TIME).until(EC.visibility_of(usage_element)) 587 | 588 | # 获取用电量的数据 589 | days_element = driver.find_elements(By.XPATH, 590 | "//*[@id='pane-second']/div[2]/div[2]/div[1]/div[3]/table/tbody/tr") # 用电量值列表 591 | date = [] 592 | usages = [] 593 | # 将用电量保存为字典 594 | for i in days_element: 595 | day = i.find_element(By.XPATH, "td[1]/div").text 596 | usage = i.find_element(By.XPATH, "td[2]/div").text 597 | if usage != "": 598 | usages.append(usage) 599 | date.append(day) 600 | else: 601 | logging.info(f"The electricity consumption of {usage} get nothing") 602 | return date, usages 603 | 604 | def _save_user_data(self, user_id, balance, last_daily_date, last_daily_usage, date, usages, month, month_usage, month_charge, yearly_charge, yearly_usage): 605 | # 连接数据库集合 606 | if self.connect_user_db(user_id): 607 | # 写入当前户号 608 | dic = {'name': 'user', 'value': f"{user_id}"} 609 | self.insert_expand_data(dic) 610 | # 写入剩余金额 611 | dic = {'name': 'balance', 'value': f"{balance}"} 612 | self.insert_expand_data(dic) 613 | # 写入最近一次更新时间 614 | dic = {'name': f"daily_date", 'value': f"{last_daily_date}"} 615 | self.insert_expand_data(dic) 616 | # 写入最近一次更新时间用电量 617 | dic = {'name': f"daily_usage", 'value': f"{last_daily_usage}"} 618 | self.insert_expand_data(dic) 619 | 620 | # 写入年用电量 621 | dic = {'name': 'yearly_usage', 'value': f"{yearly_usage}"} 622 | self.insert_expand_data(dic) 623 | # 写入年用电电费 624 | dic = {'name': 'yearly_charge', 'value': f"{yearly_charge} "} 625 | self.insert_expand_data(dic) 626 | 627 | for index in range(len(date)): 628 | dic = {'date': date[index], 'usage': float(usages[index])} 629 | # 插入到数据库 630 | try: 631 | self.insert_data(dic) 632 | logging.info(f"The electricity consumption of {usages[index]}KWh on {date[index]} has been successfully deposited into the database") 633 | except Exception as e: 634 | logging.debug(f"The electricity consumption of {date[index]} failed to save to the database, which may already exist: {str(e)}") 635 | 636 | for index in range(len(month)): 637 | try: 638 | dic = {'name': f"{month[index]}usage", 'value': f"{month_usage[index]}"} 639 | self.insert_expand_data(dic) 640 | dic = {'name': f"{month[index]}charge", 'value': f"{month_charge[index]}"} 641 | self.insert_expand_data(dic) 642 | except Exception as e: 643 | logging.debug(f"The electricity consumption of {month[index]} failed to save to the database, which may already exist: {str(e)}") 644 | if month_charge: 645 | month_charge = month_charge[-1] 646 | else: 647 | month_charge = None 648 | 649 | if month_usage: 650 | month_usage = month_usage[-1] 651 | else: 652 | month_usage = None 653 | # 写入本月电量 654 | dic = {'name': f"month_usage", 'value': f"{month_usage}"} 655 | self.insert_expand_data(dic) 656 | # 写入本月电费 657 | dic = {'name': f"month_charge", 'value': f"{month_charge}"} 658 | self.insert_expand_data(dic) 659 | # dic = {'date': month[index], 'usage': float(month_usage[index]), 'charge': float(month_charge[index])} 660 | self.connect.close() 661 | else: 662 | logging.info("The database creation failed and the data was not written correctly.") 663 | return 664 | 665 | if __name__ == "__main__": 666 | with open("bg.jpg", "rb") as f: 667 | test1 = f.read() 668 | print(type(test1)) 669 | print(test1) 670 | -------------------------------------------------------------------------------- /scripts/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | import os 4 | import sys 5 | import time 6 | import schedule 7 | import json 8 | from datetime import datetime,timedelta 9 | from const import * 10 | from data_fetcher import DataFetcher 11 | 12 | 13 | def main(): 14 | global RETRY_TIMES_LIMIT 15 | if 'PYTHON_IN_DOCKER' not in os.environ: 16 | # 读取 .env 文件 17 | import dotenv 18 | dotenv.load_dotenv(verbose=True) 19 | if os.path.isfile('/data/options.json'): 20 | with open('/data/options.json') as f: 21 | options = json.load(f) 22 | try: 23 | PHONE_NUMBER = options.get("PHONE_NUMBER") 24 | PASSWORD = options.get("PASSWORD") 25 | HASS_URL = options.get("HASS_URL") 26 | JOB_START_TIME = options.get("JOB_START_TIME", "07:00") 27 | LOG_LEVEL = options.get("LOG_LEVEL", "INFO") 28 | VERSION = os.getenv("VERSION") 29 | RETRY_TIMES_LIMIT = int(options.get("RETRY_TIMES_LIMIT", 5)) 30 | 31 | logger_init(LOG_LEVEL) 32 | os.environ["HASS_URL"] = options.get("HASS_URL", "http://homeassistant.local:8123/") 33 | os.environ["HASS_TOKEN"] = options.get("HASS_TOKEN", "") 34 | os.environ["ENABLE_DATABASE_STORAGE"] = str(options.get("ENABLE_DATABASE_STORAGE", "false")).lower() 35 | os.environ["IGNORE_USER_ID"] = options.get("IGNORE_USER_ID", "xxxxx,xxxxx") 36 | os.environ["DB_NAME"] = options.get("DB_NAME", "homeassistant.db") 37 | os.environ["RETRY_TIMES_LIMIT"] = str(options.get("RETRY_TIMES_LIMIT", 5)) 38 | os.environ["DRIVER_IMPLICITY_WAIT_TIME"] = str(options.get("DRIVER_IMPLICITY_WAIT_TIME", 60)) 39 | os.environ["LOGIN_EXPECTED_TIME"] = str(options.get("LOGIN_EXPECTED_TIME", 10)) 40 | os.environ["RETRY_WAIT_TIME_OFFSET_UNIT"] = str(options.get("RETRY_WAIT_TIME_OFFSET_UNIT", 10)) 41 | os.environ["DATA_RETENTION_DAYS"] = str(options.get("DATA_RETENTION_DAYS", 7)) 42 | os.environ["RECHARGE_NOTIFY"] = str(options.get("RECHARGE_NOTIFY", "false")).lower() 43 | os.environ["BALANCE"] = str(options.get("BALANCE", 5.0)) 44 | os.environ["PUSHPLUS_TOKEN"] = options.get("PUSHPLUS_TOKEN", "") 45 | logging.info(f"当前以Homeassistant Add-on 形式运行.") 46 | except Exception as e: 47 | logging.error(f"Failing to read the options.json file, the program will exit with an error message: {e}.") 48 | sys.exit() 49 | else: 50 | try: 51 | PHONE_NUMBER = os.getenv("PHONE_NUMBER") 52 | PASSWORD = os.getenv("PASSWORD") 53 | HASS_URL = os.getenv("HASS_URL") 54 | JOB_START_TIME = os.getenv("JOB_START_TIME","07:00" ) 55 | LOG_LEVEL = os.getenv("LOG_LEVEL","INFO") 56 | VERSION = os.getenv("VERSION") 57 | RETRY_TIMES_LIMIT = int(os.getenv("RETRY_TIMES_LIMIT", 5)) 58 | 59 | logger_init(LOG_LEVEL) 60 | logging.info(f"The current run runs as a docker image.") 61 | except Exception as e: 62 | logging.error(f"Failing to read the .env file, the program will exit with an error message: {e}.") 63 | sys.exit() 64 | 65 | logging.info(f"The current repository version is {VERSION}, and the repository address is https://github.com/ARC-MX/sgcc_electricity_new.git") 66 | current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 67 | logging.info(f"The current date is {current_datetime}.") 68 | 69 | fetcher = DataFetcher(PHONE_NUMBER, PASSWORD) 70 | logging.info(f"The current logged-in user name is {PHONE_NUMBER}, the homeassistant address is {HASS_URL}, and the program will be executed every day at {JOB_START_TIME}.") 71 | 72 | next_run_time = datetime.strptime(JOB_START_TIME, "%H:%M") + timedelta(hours=12) 73 | logging.info(f'Run job now! The next run will be at {JOB_START_TIME} and {next_run_time.strftime("%H:%M")} every day') 74 | schedule.every().day.at(JOB_START_TIME).do(run_task, fetcher) 75 | schedule.every().day.at(next_run_time.strftime("%H:%M")).do(run_task, fetcher) 76 | run_task(fetcher) 77 | 78 | while True: 79 | schedule.run_pending() 80 | time.sleep(1) 81 | 82 | 83 | def run_task(data_fetcher: DataFetcher): 84 | for retry_times in range(1, RETRY_TIMES_LIMIT + 1): 85 | try: 86 | data_fetcher.fetch() 87 | return 88 | except Exception as e: 89 | logging.error(f"state-refresh task failed, reason is [{e}], {RETRY_TIMES_LIMIT - retry_times} retry times left.") 90 | continue 91 | 92 | def logger_init(level: str): 93 | logger = logging.getLogger() 94 | logger.setLevel(level) 95 | logging.getLogger("urllib3").setLevel(logging.CRITICAL) 96 | format = logging.Formatter("%(asctime)s [%(levelname)-8s] ---- %(message)s", "%Y-%m-%d %H:%M:%S") 97 | sh = logging.StreamHandler(stream=sys.stdout) 98 | sh.setFormatter(format) 99 | logger.addHandler(sh) 100 | 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /scripts/onnx.py: -------------------------------------------------------------------------------- 1 | # import cv2 2 | from PIL import ImageDraw,Image,ImageOps 3 | import numpy as np 4 | import onnxruntime 5 | 6 | anchors = [[(116,90),(156,198),(373,326)],[(30,61),(62,45),(59,119)],[(10,13),(16,30),(33,23)]] 7 | anchors_yolo_tiny = [[(81, 82), (135, 169), (344, 319)], [(10, 14), (23, 27), (37, 58)]] 8 | CLASSES=["target"] 9 | 10 | 11 | 12 | class ONNX: 13 | def __init__(self,onnx_file_name="captcha.onnx"): 14 | self.onnx_session = onnxruntime.InferenceSession(onnx_file_name) 15 | 16 | # sigmoid函数 17 | def sigmoid(self,x): 18 | s = 1 / (1 + np.exp(-1 * x)) 19 | return s 20 | 21 | 22 | # 获取预测正确的类别,以及概率和索引; 23 | def get_result(self,class_scores): 24 | class_score = 0 25 | class_index = 0 26 | for i in range(len(class_scores)): 27 | if class_scores[i] > class_score: 28 | class_index += 1 29 | class_score = class_scores[i] 30 | return class_score, class_index 31 | 32 | 33 | def xywh2xyxy(self,x): 34 | # [x, y, w, h] to [x1, y1, x2, y2] 35 | y = np.copy(x) 36 | y[:, 0] = x[:, 0] - x[:, 2] / 2 37 | y[:, 1] = x[:, 1] - x[:, 3] / 2 38 | y[:, 2] = x[:, 0] + x[:, 2] / 2 39 | y[:, 3] = x[:, 1] + x[:, 3] / 2 40 | return y 41 | 42 | # dets: array [x,6] 6个值分别为x1,y1,x2,y2,score,class 43 | # thresh: 阈值 44 | def nms(self,dets, thresh): 45 | # dets:x1 y1 x2 y2 score class 46 | # x[:,n]就是取所有集合的第n个数据 47 | x1 = dets[:, 0] 48 | y1 = dets[:, 1] 49 | x2 = dets[:, 2] 50 | y2 = dets[:, 3] 51 | # ------------------------------------------------------- 52 | # 计算框的面积 53 | # 置信度从大到小排序 54 | # ------------------------------------------------------- 55 | areas = (y2 - y1 + 1) * (x2 - x1 + 1) 56 | scores = dets[:, 4] 57 | # print(scores) 58 | keep = [] 59 | index = scores.argsort()[::-1] # np.argsort()对某维度从小到大排序 60 | # [::-1] 从最后一个元素到第一个元素复制一遍。倒序从而从大到小排序 61 | 62 | while index.size > 0: 63 | i = index[0] 64 | keep.append(i) 65 | # ------------------------------------------------------- 66 | # 计算相交面积 67 | # 1.相交 68 | # 2.不相交 69 | # ------------------------------------------------------- 70 | x11 = np.maximum(x1[i], x1[index[1:]]) 71 | y11 = np.maximum(y1[i], y1[index[1:]]) 72 | x22 = np.minimum(x2[i], x2[index[1:]]) 73 | y22 = np.minimum(y2[i], y2[index[1:]]) 74 | 75 | w = np.maximum(0, x22 - x11 + 1) 76 | h = np.maximum(0, y22 - y11 + 1) 77 | 78 | overlaps = w * h 79 | # ------------------------------------------------------- 80 | # 计算该框与其它框的IOU,去除掉重复的框,即IOU值大的框 81 | # IOU小于thresh的框保留下来 82 | # ------------------------------------------------------- 83 | ious = overlaps / (areas[i] + areas[index[1:]] - overlaps) 84 | idx = np.where(ious <= thresh)[0] 85 | index = index[idx + 1] 86 | return keep 87 | 88 | 89 | def draw(self,image, box_data): 90 | # ------------------------------------------------------- 91 | # 取整,方便画框 92 | # ------------------------------------------------------- 93 | 94 | boxes = box_data[..., :4].astype(np.int32) # x1 x2 y1 y2 95 | scores = box_data[..., 4] 96 | classes = box_data[..., 5].astype(np.int32) 97 | for box, score, cl in zip(boxes, scores, classes): 98 | top, left, right, bottom = box 99 | # print('class: {}, score: {}'.format(CLASSES[cl], score)) 100 | # print('box coordinate left,top,right,down: [{}, {}, {}, {}]'.format(top, left, right, bottom)) 101 | # image = cv2.rectangle(image, (top, left), (right, bottom), (0, 0, 255), 1) 102 | draw = ImageDraw.Draw(image) 103 | draw.rectangle([(top, left), (right, bottom)], outline ="red") 104 | # cv2.imwrite("result"+str(left)+".jpg",image) 105 | # font = ImageFont.truetype(font='PingFang.ttc', size=40) 106 | draw.text(xy=(top, left),text='{0} {1:.2f}'.format(CLASSES[cl], score), fill=(255, 0, 0)) 107 | 108 | # image = cv2.putText(image, '{0} {1:.2f}'.format(CLASSES[cl], score), 109 | # (top, left), 110 | # cv2.FONT_HERSHEY_SIMPLEX, 111 | # 0.6, (0, 0, 255), 2) 112 | return image 113 | 114 | # 获取预测框 115 | def get_boxes(self, prediction, confidence_threshold=0.7, nms_threshold=0.6): 116 | # 过滤掉无用的框 117 | # ------------------------------------------------------- 118 | # 删除为1的维度 119 | # 删除置信度小于conf_thres的BOX 120 | # ------------------------------------------------------- 121 | # for i in range(len(prediction)): 122 | feature_map = np.squeeze(prediction)# 删除数组形状中单维度条目(shape中为1的维度) 123 | # […,4]:代表了取最里边一层的所有第4号元素,…代表了对:,:,:,等所有的的省略。此处生成:25200个第四号元素组成的数组 124 | conf = feature_map[..., 4] > confidence_threshold # 0 1 2 3 4 4是置信度,只要置信度 > conf_thres 的 125 | box = feature_map[conf == True] # 根据objectness score生成(n, 5+class_nm),只留下符合要求的框 126 | 127 | # ------------------------------------------------------- 128 | # 通过argmax获取置信度最大的类别 129 | # ------------------------------------------------------- 130 | cls_cinf = box[..., 5:] # 左闭右开(5 6 7 8),就只剩下了每个grid cell中各类别的概率 131 | cls = [] 132 | for i in range(len(cls_cinf)): 133 | cls.append(int(np.argmax(cls_cinf[i]))) # 剩下的objecctness score比较大的grid cell,分别对应的预测类别列表 134 | all_cls = list(set(cls)) # 去重,找出图中都有哪些类别 135 | # set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。 136 | # ------------------------------------------------------- 137 | # 分别对每个类别进行过滤 138 | # 1.将第6列元素替换为类别下标 139 | # 2.xywh2xyxy 坐标转换 140 | # 3.经过非极大抑制后输出的BOX下标 141 | # 4.利用下标取出非极大抑制后的BOX 142 | # ------------------------------------------------------- 143 | output = [] 144 | for i in range(len(all_cls)): 145 | curr_cls = all_cls[i] 146 | curr_cls_box = [] 147 | curr_out_box = [] 148 | 149 | for j in range(len(cls)): 150 | if cls[j] == curr_cls: 151 | box[j][5] = curr_cls 152 | curr_cls_box.append(box[j][:6]) # 左闭右开,0 1 2 3 4 5 153 | 154 | curr_cls_box = np.array(curr_cls_box) # 0 1 2 3 4 5 分别是 x y w h score class 155 | curr_cls_box = self.xywh2xyxy(curr_cls_box) # 0 1 2 3 4 5 分别是 x1 y1 x2 y2 score class 156 | curr_out_box = self.nms(curr_cls_box, nms_threshold) # 获得nms后,剩下的类别在curr_cls_box中的下标 157 | 158 | for k in curr_out_box: 159 | output.append(curr_cls_box[k]) 160 | output = np.array(output) 161 | return output 162 | 163 | def letterbox(self, img, new_shape=(640, 640), color=(114, 114, 114), auto=False, scaleFill=False, scaleup=True, 164 | stride=32): 165 | '''图片归一化''' 166 | # Resize and pad image while meeting stride-multiple constraints 167 | shape = img.shape[:2] # current shape [height, width] 168 | if isinstance(new_shape, int): 169 | new_shape = (new_shape, new_shape) 170 | 171 | # Scale ratio (new / old) 172 | r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) 173 | if not scaleup: # only scale down, do not scale up (for better test mAP) 174 | r = min(r, 1.0) 175 | 176 | # Compute padding 177 | ratio = r, r # width, height ratios 178 | 179 | new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) 180 | dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding 181 | 182 | if auto: # minimum rectangle 183 | dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding 184 | elif scaleFill: # stretch 185 | dw, dh = 0.0, 0.0 186 | new_unpad = (new_shape[1], new_shape[0]) 187 | ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios 188 | 189 | dw /= 2 # divide padding into 2 sides 190 | dh /= 2 191 | 192 | if shape[::-1] != new_unpad: # resize 193 | # img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) 194 | img = img.resize(new_unpad) 195 | top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) 196 | left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) 197 | 198 | # img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border 199 | img = ImageOps.expand(img, border=(left, top, right, bottom), fill=0)##left,top,right,bottom 200 | return img, ratio, (dw, dh) 201 | 202 | def _inference(self,image): 203 | # org_img = cv2.resize(image, [416, 416]) # resize后的原图 (640, 640, 3) 204 | org_img = image.resize((416,416)) 205 | # img = cv2.cvtColor(org_img, cv2.COLOR_BGR2RGB).transpose(2, 0, 1) 206 | img = org_img.convert("RGB") 207 | img = np.array(img).transpose(2, 0, 1) 208 | img = img.astype(dtype=np.float32) # onnx模型的类型是type: float32[ , , , ] 209 | img /= 255.0 210 | img = np.expand_dims(img, axis=0) # [3, 640, 640]扩展为[1, 3, 640, 640] 211 | 212 | inputs = {self.onnx_session.get_inputs()[0].name: img} 213 | prediction = self.onnx_session.run(None, inputs)[0] 214 | return prediction, org_img 215 | 216 | def get_distance(self,image,draw=False): 217 | prediction, org_img = self._inference(image) 218 | boxes = self.get_boxes(prediction=prediction) 219 | if len(boxes) == 0: 220 | print('No gaps were detected.') 221 | return 0 222 | else: 223 | if draw: 224 | org_img = self.draw(org_img, boxes) 225 | # cv2.imshow('result', org_img) 226 | # cv2.imwrite('result.png', org_img) 227 | org_img.save('result.png') 228 | # cv2.waitKey(0) 229 | return int(boxes[..., :4].astype(np.int32)[0][0]) 230 | 231 | if __name__ == "__main__": 232 | onnx = ONNX() 233 | img_path="../assets/background.png" 234 | # img = cv2.imread(img_path) 235 | img = Image.open(img_path) 236 | print(onnx.get_distance(img,True)) -------------------------------------------------------------------------------- /scripts/sensor_updator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from datetime import datetime,timedelta 4 | 5 | import requests 6 | from sympy import true 7 | 8 | from const import * 9 | 10 | 11 | class SensorUpdator: 12 | 13 | def __init__(self): 14 | HASS_URL = os.getenv("HASS_URL") 15 | HASS_TOKEN = os.getenv("HASS_TOKEN") 16 | self.base_url = HASS_URL[:-1] if HASS_URL.endswith("/") else HASS_URL 17 | self.token = HASS_TOKEN 18 | self.RECHARGE_NOTIFY = os.getenv("RECHARGE_NOTIFY", "false").lower() == "true" 19 | 20 | def update_one_userid(self, user_id: str, balance: float, last_daily_date: str, last_daily_usage: float, yearly_charge: float, yearly_usage: float, month_charge: float, month_usage: float): 21 | postfix = f"_{user_id[-4:]}" 22 | if balance is not None: 23 | self.balance_notify(user_id, balance) 24 | self.update_balance(postfix, balance) 25 | if last_daily_usage is not None: 26 | self.update_last_daily_usage(postfix, last_daily_date, last_daily_usage) 27 | if yearly_usage is not None: 28 | self.update_yearly_data(postfix, yearly_usage, usage=True) 29 | if yearly_charge is not None: 30 | self.update_yearly_data(postfix, yearly_charge) 31 | if month_usage is not None: 32 | self.update_month_data(postfix, month_usage, usage=True) 33 | if month_charge is not None: 34 | self.update_month_data(postfix, month_charge) 35 | 36 | logging.info(f"User {user_id} state-refresh task run successfully!") 37 | 38 | def update_last_daily_usage(self, postfix: str, last_daily_date: str, sensorState: float): 39 | sensorName = DAILY_USAGE_SENSOR_NAME + postfix 40 | request_body = { 41 | "state": sensorState, 42 | "unique_id": sensorName, 43 | "attributes": { 44 | "last_reset": last_daily_date, 45 | "unit_of_measurement": "kWh", 46 | "icon": "mdi:lightning-bolt", 47 | "device_class": "energy", 48 | "state_class": "measurement", 49 | }, 50 | } 51 | 52 | self.send_url(sensorName, request_body) 53 | logging.info(f"Homeassistant sensor {sensorName} state updated: {sensorState} kWh") 54 | 55 | def update_balance(self, postfix: str, sensorState: float): 56 | sensorName = BALANCE_SENSOR_NAME + postfix 57 | last_reset = datetime.now().strftime("%Y-%m-%d, %H:%M:%S") 58 | request_body = { 59 | "state": sensorState, 60 | "unique_id": sensorName, 61 | "attributes": { 62 | "last_reset": last_reset, 63 | "unit_of_measurement": "CNY", 64 | "icon": "mdi:cash", 65 | "device_class": "monetary", 66 | "state_class": "total", 67 | }, 68 | } 69 | 70 | self.send_url(sensorName, request_body) 71 | logging.info(f"Homeassistant sensor {sensorName} state updated: {sensorState} CNY") 72 | 73 | def update_month_data(self, postfix: str, sensorState: float, usage=False): 74 | sensorName = ( 75 | MONTH_USAGE_SENSOR_NAME + postfix 76 | if usage 77 | else MONTH_CHARGE_SENSOR_NAME + postfix 78 | ) 79 | current_date = datetime.now() 80 | first_day_of_current_month = current_date.replace(day=1) 81 | last_day_of_previous_month = first_day_of_current_month - timedelta(days=1) 82 | last_reset = last_day_of_previous_month.strftime("%Y-%m") 83 | request_body = { 84 | "state": sensorState, 85 | "unique_id": sensorName, 86 | "attributes": { 87 | "last_reset": last_reset, 88 | "unit_of_measurement": "kWh" if usage else "CNY", 89 | "icon": "mdi:lightning-bolt" if usage else "mdi:cash", 90 | "device_class": "energy" if usage else "monetary", 91 | "state_class": "measurement", 92 | }, 93 | } 94 | 95 | self.send_url(sensorName, request_body) 96 | logging.info(f"Homeassistant sensor {sensorName} state updated: {sensorState} {'kWh' if usage else 'CNY'}") 97 | 98 | def update_yearly_data(self, postfix: str, sensorState: float, usage=False): 99 | sensorName = ( 100 | YEARLY_USAGE_SENSOR_NAME + postfix 101 | if usage 102 | else YEARLY_CHARGE_SENSOR_NAME + postfix 103 | ) 104 | if datetime.now().month == 1: 105 | last_year = datetime.now().year -1 106 | last_reset = datetime.now().replace(year=last_year).strftime("%Y") 107 | else: 108 | last_reset = datetime.now().strftime("%Y") 109 | request_body = { 110 | "state": sensorState, 111 | "unique_id": sensorName, 112 | "attributes": { 113 | "last_reset": last_reset, 114 | "unit_of_measurement": "kWh" if usage else "CNY", 115 | "icon": "mdi:lightning-bolt" if usage else "mdi:cash", 116 | "device_class": "energy" if usage else "monetary", 117 | "state_class": "total_increasing", 118 | }, 119 | } 120 | self.send_url(sensorName, request_body) 121 | logging.info(f"Homeassistant sensor {sensorName} state updated: {sensorState} {'kWh' if usage else 'CNY'}") 122 | 123 | def send_url(self, sensorName, request_body): 124 | headers = { 125 | "Content-Type": "application-json", 126 | "Authorization": "Bearer " + self.token, 127 | } 128 | url = self.base_url + API_PATH + sensorName # /api/states/ 129 | try: 130 | response = requests.post(url, json=request_body, headers=headers) 131 | logging.debug( 132 | f"Homeassistant REST API invoke, POST on {url}. response[{response.status_code}]: {response.content}" 133 | ) 134 | except Exception as e: 135 | logging.error(f"Homeassistant REST API invoke failed, reason is {e}") 136 | 137 | def balance_notify(self, user_id, balance): 138 | 139 | if self.RECHARGE_NOTIFY : 140 | BALANCE = float(os.getenv("BALANCE", 10.0)) 141 | PUSHPLUS_TOKEN = os.getenv("PUSHPLUS_TOKEN").split(",") 142 | logging.info(f"Check the electricity bill balance. When the balance is less than {BALANCE} CNY, the notification will be sent = {self.RECHARGE_NOTIFY}") 143 | if balance < BALANCE : 144 | for token in PUSHPLUS_TOKEN: 145 | title = "电费余额不足提醒" 146 | content = (f"您用户号{user_id}的当前电费余额为:{balance}元,请及时充值。" ) 147 | url = ("http://www.pushplus.plus/send?token="+ token+ "&title="+ title+ "&content="+ content) 148 | requests.get(url) 149 | logging.info( 150 | f"The current balance of user id {user_id} is {balance} CNY less than {BALANCE} CNY, notice has been sent, please pay attention to check and recharge." 151 | ) 152 | else : 153 | logging.info( 154 | f"Check the electricity bill balance, the notification will be sent = {self.RECHARGE_NOTIFY}") 155 | return 156 | 157 | --------------------------------------------------------------------------------