├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── matchers │ ├── check-executables-have-shebangs.json │ ├── check-json.json │ ├── codespell.json │ ├── flake8.json │ └── yamllint.json │ ├── python-package.yml │ ├── python-publish.yml │ └── release_draft.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .secretlintrc.json ├── .vscode ├── launch.json └── settings.json ├── .yamllint ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── dev.env ├── package.json ├── pyhiveapi └── apyhiveapi │ ├── __init__.py │ ├── action.py │ ├── alarm.py │ ├── api │ ├── __init__.py │ ├── hive_api.py │ ├── hive_async_api.py │ ├── hive_auth.py │ └── hive_auth_async.py │ ├── camera.py │ ├── data │ ├── alarm.json │ ├── camera.json │ └── data.json │ ├── device_attributes.py │ ├── heating.py │ ├── helper │ ├── __init__.py │ ├── const.py │ ├── debugger.py │ ├── hive_exceptions.py │ ├── hive_helper.py │ ├── hivedataclasses.py │ ├── logger.py │ └── map.py │ ├── hive.py │ ├── hotwater.py │ ├── hub.py │ ├── light.py │ ├── plug.py │ ├── sensor.py │ └── session.py ├── pyproject.toml ├── requirements.txt ├── requirements_test.txt ├── setup.cfg ├── setup.py └── tests ├── API └── async_auth.py ├── __init__.py ├── bandit.yaml ├── common.py └── test_hub.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: KJonline, Rendili 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: KJonline, Rendili 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: "v$RESOLVED_VERSION" 2 | tag-template: "v$RESOLVED_VERSION" 3 | template: | 4 | # What's Changed 5 | 6 | $CHANGES 7 | categories: 8 | - title: "Breaking" 9 | label: "type: breaking" 10 | - title: "New" 11 | label: "type: feature" 12 | - title: "Bug Fixes" 13 | label: "type: bug" 14 | - title: "Maintenance" 15 | label: "type: maintenance" 16 | - title: "Documentation" 17 | label: "type: docs" 18 | - title: "Dependency Updates" 19 | label: "type: dependencies" 20 | 21 | version-resolver: 22 | major: 23 | labels: 24 | - "type: breaking" 25 | minor: 26 | labels: 27 | - "type: feature" 28 | patch: 29 | labels: 30 | - "type: bug" 31 | - "type: maintenance" 32 | - "type: docs" 33 | - "type: dependencies" 34 | - "type: security" 35 | 36 | exclude-labels: 37 | - "skip-changelog" 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | pull_request: ~ 10 | 11 | env: 12 | CACHE_VERSION: 1 13 | DEFAULT_PYTHON: 3.8 14 | PRE_COMMIT_HOME: ~/.cache/pre-commit 15 | 16 | jobs: 17 | # Separate job to pre-populate the base dependency cache 18 | # This prevent upcoming jobs to do the same individually 19 | prepare-base: 20 | name: Prepare base dependencies 21 | runs-on: ubuntu-20.04 22 | steps: 23 | - name: Check out code from GitHub 24 | uses: actions/checkout@v2 25 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 26 | id: python 27 | uses: actions/setup-python@v2.2.1 28 | with: 29 | python-version: ${{ env.DEFAULT_PYTHON }} 30 | - name: Restore base Python virtual environment 31 | id: cache-venv 32 | uses: actions/cache@v2.1.4 33 | with: 34 | path: venv 35 | key: >- 36 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 37 | steps.python.outputs.python-version }}-${{ 38 | hashFiles('requirements.txt') }}-${{ 39 | hashFiles('requirements_test.txt') }} 40 | restore-keys: | 41 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- 42 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} 43 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- 44 | - name: Create Python virtual environment 45 | if: steps.cache-venv.outputs.cache-hit != 'true' 46 | run: | 47 | python -m venv venv 48 | . venv/bin/activate 49 | pip install -U "pip<20.3" setuptools 50 | pip install -r requirements.txt -r requirements_test.txt 51 | - name: Restore pre-commit environment from cache 52 | id: cache-precommit 53 | uses: actions/cache@v2.1.4 54 | with: 55 | path: ${{ env.PRE_COMMIT_HOME }} 56 | key: | 57 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 58 | restore-keys: | 59 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- 60 | - name: Install pre-commit dependencies 61 | if: steps.cache-precommit.outputs.cache-hit != 'true' 62 | run: | 63 | . venv/bin/activate 64 | pre-commit install-hooks 65 | 66 | lint-bandit: 67 | name: Check bandit 68 | runs-on: ubuntu-20.04 69 | needs: prepare-base 70 | steps: 71 | - name: Check out code from GitHub 72 | uses: actions/checkout@v2 73 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 74 | uses: actions/setup-python@v2.2.1 75 | id: python 76 | with: 77 | python-version: ${{ env.DEFAULT_PYTHON }} 78 | - name: Restore base Python virtual environment 79 | id: cache-venv 80 | uses: actions/cache@v2.1.4 81 | with: 82 | path: venv 83 | key: >- 84 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 85 | steps.python.outputs.python-version }}-${{ 86 | hashFiles('requirements.txt') }}-${{ 87 | hashFiles('requirements_test.txt') }} 88 | - name: Fail job if Python cache restore failed 89 | if: steps.cache-venv.outputs.cache-hit != 'true' 90 | run: | 91 | echo "Failed to restore Python virtual environment from cache" 92 | exit 1 93 | - name: Restore pre-commit environment from cache 94 | id: cache-precommit 95 | uses: actions/cache@v2.1.4 96 | with: 97 | path: ${{ env.PRE_COMMIT_HOME }} 98 | key: | 99 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 100 | - name: Fail job if cache restore failed 101 | if: steps.cache-venv.outputs.cache-hit != 'true' 102 | run: | 103 | echo "Failed to restore Python virtual environment from cache" 104 | exit 1 105 | - name: Run bandit 106 | run: | 107 | . venv/bin/activate 108 | pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure 109 | 110 | lint-black: 111 | name: Check black 112 | runs-on: ubuntu-20.04 113 | needs: prepare-base 114 | steps: 115 | - name: Check out code from GitHub 116 | uses: actions/checkout@v2 117 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 118 | uses: actions/setup-python@v2.2.1 119 | id: python 120 | with: 121 | python-version: ${{ env.DEFAULT_PYTHON }} 122 | - name: Restore base Python virtual environment 123 | id: cache-venv 124 | uses: actions/cache@v2.1.4 125 | with: 126 | path: venv 127 | key: >- 128 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 129 | steps.python.outputs.python-version }}-${{ 130 | hashFiles('requirements.txt') }}-${{ 131 | hashFiles('requirements_test.txt') }} 132 | - name: Fail job if Python cache restore failed 133 | if: steps.cache-venv.outputs.cache-hit != 'true' 134 | run: | 135 | echo "Failed to restore Python virtual environment from cache" 136 | exit 1 137 | - name: Restore pre-commit environment from cache 138 | id: cache-precommit 139 | uses: actions/cache@v2.1.4 140 | with: 141 | path: ${{ env.PRE_COMMIT_HOME }} 142 | key: | 143 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 144 | - name: Fail job if cache restore failed 145 | if: steps.cache-venv.outputs.cache-hit != 'true' 146 | run: | 147 | echo "Failed to restore Python virtual environment from cache" 148 | exit 1 149 | - name: Run black 150 | run: | 151 | . venv/bin/activate 152 | pre-commit run --hook-stage manual black --all-files --show-diff-on-failure 153 | 154 | lint-codespell: 155 | name: Check codespell 156 | runs-on: ubuntu-20.04 157 | needs: prepare-base 158 | steps: 159 | - name: Check out code from GitHub 160 | uses: actions/checkout@v2 161 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 162 | uses: actions/setup-python@v2.2.1 163 | id: python 164 | with: 165 | python-version: ${{ env.DEFAULT_PYTHON }} 166 | - name: Restore base Python virtual environment 167 | id: cache-venv 168 | uses: actions/cache@v2.1.4 169 | with: 170 | path: venv 171 | key: >- 172 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 173 | steps.python.outputs.python-version }}-${{ 174 | hashFiles('requirements.txt') }}-${{ 175 | hashFiles('requirements_test.txt') }} 176 | - name: Fail job if Python cache restore failed 177 | if: steps.cache-venv.outputs.cache-hit != 'true' 178 | run: | 179 | echo "Failed to restore Python virtual environment from cache" 180 | exit 1 181 | - name: Restore pre-commit environment from cache 182 | id: cache-precommit 183 | uses: actions/cache@v2.1.4 184 | with: 185 | path: ${{ env.PRE_COMMIT_HOME }} 186 | key: | 187 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 188 | - name: Fail job if cache restore failed 189 | if: steps.cache-venv.outputs.cache-hit != 'true' 190 | run: | 191 | echo "Failed to restore Python virtual environment from cache" 192 | exit 1 193 | - name: Register codespell problem matcher 194 | run: | 195 | echo "::add-matcher::.github/workflows/matchers/codespell.json" 196 | - name: Run codespell 197 | run: | 198 | . venv/bin/activate 199 | pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files 200 | 201 | lint-executable-shebangs: 202 | name: Check executables 203 | runs-on: ubuntu-20.04 204 | needs: prepare-base 205 | steps: 206 | - name: Check out code from GitHub 207 | uses: actions/checkout@v2 208 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 209 | uses: actions/setup-python@v2.2.1 210 | id: python 211 | with: 212 | python-version: ${{ env.DEFAULT_PYTHON }} 213 | - name: Restore base Python virtual environment 214 | id: cache-venv 215 | uses: actions/cache@v2.1.4 216 | with: 217 | path: venv 218 | key: >- 219 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 220 | steps.python.outputs.python-version }}-${{ 221 | hashFiles('requirements.txt') }}-${{ 222 | hashFiles('requirements_test.txt') }} 223 | - name: Fail job if Python cache restore failed 224 | if: steps.cache-venv.outputs.cache-hit != 'true' 225 | run: | 226 | echo "Failed to restore Python virtual environment from cache" 227 | exit 1 228 | - name: Restore pre-commit environment from cache 229 | id: cache-precommit 230 | uses: actions/cache@v2.1.4 231 | with: 232 | path: ${{ env.PRE_COMMIT_HOME }} 233 | key: | 234 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 235 | - name: Fail job if cache restore failed 236 | if: steps.cache-venv.outputs.cache-hit != 'true' 237 | run: | 238 | echo "Failed to restore Python virtual environment from cache" 239 | exit 1 240 | - name: Register check executables problem matcher 241 | run: | 242 | echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" 243 | - name: Run executables check 244 | run: | 245 | . venv/bin/activate 246 | pre-commit run --hook-stage manual check-executables-have-shebangs --all-files 247 | 248 | lint-flake8: 249 | name: Check flake8 250 | runs-on: ubuntu-20.04 251 | needs: prepare-base 252 | steps: 253 | - name: Check out code from GitHub 254 | uses: actions/checkout@v2 255 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 256 | uses: actions/setup-python@v2.2.1 257 | id: python 258 | with: 259 | python-version: ${{ env.DEFAULT_PYTHON }} 260 | - name: Restore base Python virtual environment 261 | id: cache-venv 262 | uses: actions/cache@v2.1.4 263 | with: 264 | path: venv 265 | key: >- 266 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 267 | steps.python.outputs.python-version }}-${{ 268 | hashFiles('requirements.txt') }}-${{ 269 | hashFiles('requirements_test.txt') }} 270 | - name: Fail job if Python cache restore failed 271 | if: steps.cache-venv.outputs.cache-hit != 'true' 272 | run: | 273 | echo "Failed to restore Python virtual environment from cache" 274 | exit 1 275 | - name: Restore pre-commit environment from cache 276 | id: cache-precommit 277 | uses: actions/cache@v2.1.4 278 | with: 279 | path: ${{ env.PRE_COMMIT_HOME }} 280 | key: | 281 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 282 | - name: Fail job if cache restore failed 283 | if: steps.cache-venv.outputs.cache-hit != 'true' 284 | run: | 285 | echo "Failed to restore Python virtual environment from cache" 286 | exit 1 287 | - name: Register flake8 problem matcher 288 | run: | 289 | echo "::add-matcher::.github/workflows/matchers/flake8.json" 290 | - name: Run flake8 291 | run: | 292 | . venv/bin/activate 293 | pre-commit run --hook-stage manual flake8 --all-files 294 | 295 | lint-isort: 296 | name: Check isort 297 | runs-on: ubuntu-20.04 298 | needs: prepare-base 299 | steps: 300 | - name: Check out code from GitHub 301 | uses: actions/checkout@v2 302 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 303 | uses: actions/setup-python@v2.2.1 304 | id: python 305 | with: 306 | python-version: ${{ env.DEFAULT_PYTHON }} 307 | - name: Restore base Python virtual environment 308 | id: cache-venv 309 | uses: actions/cache@v2.1.4 310 | with: 311 | path: venv 312 | key: >- 313 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 314 | steps.python.outputs.python-version }}-${{ 315 | hashFiles('requirements.txt') }}-${{ 316 | hashFiles('requirements_test.txt') }} 317 | - name: Fail job if Python cache restore failed 318 | if: steps.cache-venv.outputs.cache-hit != 'true' 319 | run: | 320 | echo "Failed to restore Python virtual environment from cache" 321 | exit 1 322 | - name: Restore pre-commit environment from cache 323 | id: cache-precommit 324 | uses: actions/cache@v2.1.4 325 | with: 326 | path: ${{ env.PRE_COMMIT_HOME }} 327 | key: | 328 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 329 | - name: Fail job if cache restore failed 330 | if: steps.cache-venv.outputs.cache-hit != 'true' 331 | run: | 332 | echo "Failed to restore Python virtual environment from cache" 333 | exit 1 334 | - name: Run isort 335 | run: | 336 | . venv/bin/activate 337 | pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure 338 | 339 | lint-json: 340 | name: Check JSON 341 | runs-on: ubuntu-20.04 342 | needs: prepare-base 343 | steps: 344 | - name: Check out code from GitHub 345 | uses: actions/checkout@v2 346 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 347 | uses: actions/setup-python@v2.2.1 348 | id: python 349 | with: 350 | python-version: ${{ env.DEFAULT_PYTHON }} 351 | - name: Restore base Python virtual environment 352 | id: cache-venv 353 | uses: actions/cache@v2.1.4 354 | with: 355 | path: venv 356 | key: >- 357 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 358 | steps.python.outputs.python-version }}-${{ 359 | hashFiles('requirements.txt') }}-${{ 360 | hashFiles('requirements_test.txt') }} 361 | - name: Fail job if Python cache restore failed 362 | if: steps.cache-venv.outputs.cache-hit != 'true' 363 | run: | 364 | echo "Failed to restore Python virtual environment from cache" 365 | exit 1 366 | - name: Restore pre-commit environment from cache 367 | id: cache-precommit 368 | uses: actions/cache@v2.1.4 369 | with: 370 | path: ${{ env.PRE_COMMIT_HOME }} 371 | key: | 372 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 373 | - name: Fail job if cache restore failed 374 | if: steps.cache-venv.outputs.cache-hit != 'true' 375 | run: | 376 | echo "Failed to restore Python virtual environment from cache" 377 | exit 1 378 | - name: Register check-json problem matcher 379 | run: | 380 | echo "::add-matcher::.github/workflows/matchers/check-json.json" 381 | - name: Run check-json 382 | run: | 383 | . venv/bin/activate 384 | pre-commit run --hook-stage manual check-json --all-files 385 | 386 | lint-pyupgrade: 387 | name: Check pyupgrade 388 | runs-on: ubuntu-20.04 389 | needs: prepare-base 390 | steps: 391 | - name: Check out code from GitHub 392 | uses: actions/checkout@v2 393 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 394 | uses: actions/setup-python@v2.2.1 395 | id: python 396 | with: 397 | python-version: ${{ env.DEFAULT_PYTHON }} 398 | - name: Restore base Python virtual environment 399 | id: cache-venv 400 | uses: actions/cache@v2.1.4 401 | with: 402 | path: venv 403 | key: >- 404 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 405 | steps.python.outputs.python-version }}-${{ 406 | hashFiles('requirements.txt') }}-${{ 407 | hashFiles('requirements_test.txt') }} 408 | - name: Fail job if Python cache restore failed 409 | if: steps.cache-venv.outputs.cache-hit != 'true' 410 | run: | 411 | echo "Failed to restore Python virtual environment from cache" 412 | exit 1 413 | - name: Restore pre-commit environment from cache 414 | id: cache-precommit 415 | uses: actions/cache@v2.1.4 416 | with: 417 | path: ${{ env.PRE_COMMIT_HOME }} 418 | key: | 419 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 420 | - name: Fail job if cache restore failed 421 | if: steps.cache-venv.outputs.cache-hit != 'true' 422 | run: | 423 | echo "Failed to restore Python virtual environment from cache" 424 | exit 1 425 | - name: Run pyupgrade 426 | run: | 427 | . venv/bin/activate 428 | pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure 429 | 430 | lint-yaml: 431 | name: Check YAML 432 | runs-on: ubuntu-20.04 433 | needs: prepare-base 434 | steps: 435 | - name: Check out code from GitHub 436 | uses: actions/checkout@v2 437 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 438 | uses: actions/setup-python@v2.2.1 439 | id: python 440 | with: 441 | python-version: ${{ env.DEFAULT_PYTHON }} 442 | - name: Restore base Python virtual environment 443 | id: cache-venv 444 | uses: actions/cache@v2.1.4 445 | with: 446 | path: venv 447 | key: >- 448 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 449 | steps.python.outputs.python-version }}-${{ 450 | hashFiles('requirements.txt') }}-${{ 451 | hashFiles('requirements_test.txt') }} 452 | - name: Fail job if Python cache restore failed 453 | if: steps.cache-venv.outputs.cache-hit != 'true' 454 | run: | 455 | echo "Failed to restore Python virtual environment from cache" 456 | exit 1 457 | - name: Restore pre-commit environment from cache 458 | id: cache-precommit 459 | uses: actions/cache@v2.1.4 460 | with: 461 | path: ${{ env.PRE_COMMIT_HOME }} 462 | key: | 463 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 464 | - name: Fail job if cache restore failed 465 | if: steps.cache-venv.outputs.cache-hit != 'true' 466 | run: | 467 | echo "Failed to restore Python virtual environment from cache" 468 | exit 1 469 | - name: Register yamllint problem matcher 470 | run: | 471 | echo "::add-matcher::.github/workflows/matchers/yamllint.json" 472 | - name: Run yamllint 473 | run: | 474 | . venv/bin/activate 475 | pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure 476 | pylint: 477 | name: Check pylint 478 | runs-on: ubuntu-20.04 479 | steps: 480 | - uses: actions/checkout@v2 481 | - name: Set up python ${{ env.DEFAULT_PYTHON }} 482 | uses: actions/setup-python@v1 483 | with: 484 | python-version: ${{ env.DEFAULT_PYTHON }} 485 | - name: Install dependencies 486 | run: | 487 | python -m pip install --upgrade pip 488 | pip install pylint 489 | - name: Analysing the code with pylint 490 | run: | 491 | python -m pylint --fail-under=10 `find -regextype egrep -regex '(.*.py)$'` 492 | -------------------------------------------------------------------------------- /.github/workflows/matchers/check-executables-have-shebangs.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "check-executables-have-shebangs", 5 | "pattern": [ 6 | { 7 | "regexp": "^(.+):\\s(.+)$", 8 | "file": 1, 9 | "message": 2 10 | } 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/matchers/check-json.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "check-json", 5 | "pattern": [ 6 | { 7 | "regexp": "^(.+):\\s(.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$", 8 | "file": 1, 9 | "message": 2, 10 | "line": 3, 11 | "column": 4 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/matchers/codespell.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "codespell", 5 | "severity": "warning", 6 | "pattern": [ 7 | { 8 | "regexp": "^(.+):(\\d+):\\s(.+)$", 9 | "file": 1, 10 | "line": 2, 11 | "message": 3 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/matchers/flake8.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "flake8-error", 5 | "severity": "error", 6 | "pattern": [ 7 | { 8 | "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "column": 3, 12 | "message": 4 13 | } 14 | ] 15 | }, 16 | { 17 | "owner": "flake8-warning", 18 | "severity": "warning", 19 | "pattern": [ 20 | { 21 | "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", 22 | "file": 1, 23 | "line": 2, 24 | "column": 3, 25 | "message": 4 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/matchers/yamllint.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "yamllint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(.*\\.ya?ml)$", 8 | "file": 1 9 | }, 10 | { 11 | "regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$", 12 | "line": 1, 13 | "column": 2, 14 | "severity": 3, 15 | "message": 4, 16 | "code": 5, 17 | "loop": true 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | strategy: 13 | matrix: 14 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | python -m pip install flake8 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | - name: Lint with flake8 28 | run: | 29 | # stop the build if there are Python syntax errors. 30 | flake8 . --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. T 32 | flake8 . --max-line-length=79 --statistics 33 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package to PyPI when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | release-build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.x" 28 | 29 | - name: Build release distributions 30 | run: | 31 | # NOTE: put your own distribution build steps here. 32 | python -m pip install build 33 | python -m build 34 | 35 | - name: Upload wheel to GitHub Release 36 | uses: ncipollo/release-action@v1 37 | with: 38 | artifacts: "dist/*.whl" # Path to your wheel file(s) 39 | tag: ${{ github.ref_name }} 40 | allowUpdates: true 41 | 42 | - name: Upload distributions 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: release-dists 46 | path: dist/ 47 | 48 | pypi-publish: 49 | runs-on: ubuntu-latest 50 | needs: 51 | - release-build 52 | permissions: 53 | # IMPORTANT: this permission is mandatory for trusted publishing 54 | id-token: write 55 | 56 | # Dedicated environments with protections for publishing are strongly recommended. 57 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules 58 | environment: 59 | name: pypi 60 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status: 61 | url: https://pypi.org/project/pyhive-integration 62 | # 63 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string 64 | # ALTERNATIVE: exactly, uncomment the following line instead: 65 | # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }} 66 | 67 | steps: 68 | - name: Retrieve release distributions 69 | uses: actions/download-artifact@v4 70 | with: 71 | name: release-dists 72 | path: dist/ 73 | 74 | - name: Publish release distributions to PyPI 75 | uses: pypa/gh-action-pypi-publish@release/v1 76 | with: 77 | packages-dir: dist/ 78 | -------------------------------------------------------------------------------- /.github/workflows/release_draft.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update_release_draft: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: release-drafter/release-drafter@master 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | *.egg 6 | *.py[cod] 7 | __pycache__/ 8 | *.so 9 | *~ 10 | build 11 | dist 12 | pyhiveapi.egg-info 13 | 14 | # due to using tox and pytest 15 | .tox 16 | .cache 17 | test* 18 | custom_tests/* 19 | 20 | # virtual environment folder 21 | .venv/ 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v2.34.0 4 | hooks: 5 | - id: pyupgrade 6 | args: [--py38-plus] 7 | - repo: https://github.com/psf/black 8 | rev: 22.3.0 9 | hooks: 10 | - id: black 11 | args: 12 | - --safe 13 | - --quiet 14 | - repo: https://github.com/codespell-project/codespell 15 | rev: v2.1.0 16 | hooks: 17 | - id: codespell 18 | args: 19 | - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort 20 | - --skip="./.*,*.csv,*.json" 21 | - --quiet-level=2 22 | exclude_types: [csv, json] 23 | - repo: https://github.com/pycqa/flake8 24 | rev: 3.9.2 25 | hooks: 26 | - id: flake8 27 | additional_dependencies: 28 | - flake8-docstrings==1.5.0 29 | - pydocstyle==5.1.1 30 | - repo: https://github.com/PyCQA/bandit 31 | rev: 1.7.4 32 | hooks: 33 | - id: bandit 34 | args: 35 | - --quiet 36 | - --format=custom 37 | - --configfile=tests/bandit.yaml 38 | - repo: https://github.com/PyCQA/isort 39 | rev: 5.12.0 40 | hooks: 41 | - id: isort 42 | args: ["--profile", "black"] 43 | - repo: https://github.com/adrienverge/yamllint.git 44 | rev: v1.26.3 45 | hooks: 46 | - id: yamllint 47 | - repo: https://github.com/pre-commit/mirrors-prettier 48 | rev: v2.7.1 49 | hooks: 50 | - id: prettier 51 | stages: [manual] 52 | - repo: https://github.com/pre-commit/pre-commit-hooks 53 | rev: v4.3.0 54 | hooks: 55 | - id: check-executables-have-shebangs 56 | stages: [manual] 57 | - id: check-json 58 | exclude: (.vscode|.devcontainer) 59 | - id: no-commit-to-branch 60 | args: 61 | - --branch=master 62 | - repo: local 63 | hooks: 64 | - id: pylint 65 | name: pylint 66 | entry: pylint 67 | language: system 68 | types: [python] 69 | args: 70 | [ 71 | "-rn", # Only display messages 72 | "-sn", # Don't display the score 73 | ] 74 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, checkers without error messages are disabled and for others, 13 | # only the ERROR messages are displayed, and no reports are done by default. 14 | #errors-only= 15 | 16 | # Always return a 0 (non-error) status code, even if lint errors are found. 17 | # This is primarily useful in continuous integration scripts. 18 | #exit-zero= 19 | 20 | # A comma-separated list of package or module names from where C extensions may 21 | # be loaded. Extensions are loading into the active Python interpreter and may 22 | # run arbitrary code. 23 | extension-pkg-allow-list= 24 | 25 | # A comma-separated list of package or module names from where C extensions may 26 | # be loaded. Extensions are loading into the active Python interpreter and may 27 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 28 | # for backward compatibility.) 29 | extension-pkg-whitelist= 30 | 31 | # Return non-zero exit code if any of these messages/categories are detected, 32 | # even if score is above --fail-under value. Syntax same as enable. Messages 33 | # specified are enabled, while categories only check already-enabled messages. 34 | fail-on= 35 | 36 | # Specify a score threshold to be exceeded before program exits with error. 37 | fail-under=10 38 | 39 | # Interpret the stdin as a python script, whose filename needs to be passed as 40 | # the module_or_package argument. 41 | #from-stdin= 42 | 43 | # Files or directories to be skipped. They should be base names, not paths. 44 | ignore=CVS 45 | 46 | # Add files or directories matching the regex patterns to the ignore-list. The 47 | # regex matches against paths and can be in Posix or Windows format. 48 | ignore-paths= 49 | 50 | # Files or directories matching the regex patterns are skipped. The regex 51 | # matches against base names, not paths. The default value ignores Emacs file 52 | # locks 53 | ignore-patterns=^\.# 54 | 55 | # List of module names for which member attributes should not be checked 56 | # (useful for modules/projects where namespaces are manipulated during runtime 57 | # and thus existing member attributes cannot be deduced by static analysis). It 58 | # supports qualified module names, as well as Unix pattern matching. 59 | ignored-modules= 60 | 61 | # Python code to execute, usually for sys.path manipulation such as 62 | # pygtk.require(). 63 | #init-hook= 64 | 65 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 66 | # number of processors available to use. 67 | jobs=1 68 | 69 | # Control the amount of potential inferred values when inferring a single 70 | # object. This can help the performance when dealing with large functions or 71 | # complex, nested conditions. 72 | limit-inference-results=100 73 | 74 | # List of plugins (as comma separated values of python module names) to load, 75 | # usually to register additional checkers. 76 | load-plugins= 77 | 78 | # Pickle collected data for later comparisons. 79 | persistent=yes 80 | 81 | # Minimum Python version to use for version dependent checks. Will default to 82 | # the version used to run pylint. 83 | py-version=3.9 84 | 85 | # Discover python modules and packages in the file system subtree. 86 | recursive=no 87 | 88 | # When enabled, pylint would attempt to guess common misconfiguration and emit 89 | # user-friendly hints instead of false-positive error messages. 90 | suggestion-mode=yes 91 | 92 | # Allow loading of arbitrary C extensions. Extensions are imported into the 93 | # active Python interpreter and may run arbitrary code. 94 | unsafe-load-any-extension=no 95 | 96 | # In verbose mode, extra non-checker-related info will be displayed. 97 | #verbose= 98 | 99 | 100 | [REPORTS] 101 | 102 | # Python expression which should return a score less than or equal to 10. You 103 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 104 | # 'convention', and 'info' which contain the number of messages in each 105 | # category, as well as 'statement' which is the total number of statements 106 | # analyzed. This score is used by the global evaluation report (RP0004). 107 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 108 | 109 | # Template used to display messages. This is a python new-style format string 110 | # used to format the message information. See doc for all details. 111 | msg-template= 112 | 113 | # Set the output format. Available formats are text, parseable, colorized, json 114 | # and msvs (visual studio). You can also give a reporter class, e.g. 115 | # mypackage.mymodule.MyReporterClass. 116 | #output-format= 117 | 118 | # Tells whether to display a full report or only the messages. 119 | reports=no 120 | 121 | # Activate the evaluation score. 122 | score=yes 123 | 124 | 125 | [MESSAGES CONTROL] 126 | 127 | # Only show warnings with the listed confidence levels. Leave empty to show 128 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 129 | # UNDEFINED. 130 | confidence=HIGH, 131 | CONTROL_FLOW, 132 | INFERENCE, 133 | INFERENCE_FAILURE, 134 | UNDEFINED 135 | 136 | # Disable the message, report, category or checker with the given id(s). You 137 | # can either give multiple identifiers separated by comma (,) or put this 138 | # option multiple times (only on the command line, not in the configuration 139 | # file where it should appear only once). You can also use "--disable=all" to 140 | # disable everything first and then re-enable specific checks. For example, if 141 | # you want to run only the similarities checker, you can use "--disable=all 142 | # --enable=similarities". If you want to run only the classes checker, but have 143 | # no Warning level messages displayed, use "--disable=all --enable=classes 144 | # --disable=W". 145 | disable=raw-checker-failed, 146 | bad-inline-option, 147 | locally-disabled, 148 | file-ignored, 149 | suppressed-message, 150 | useless-suppression, 151 | deprecated-pragma, 152 | use-symbolic-message-instead, 153 | too-many-instance-attributes, 154 | too-many-arguments, 155 | too-many-branches, 156 | duplicate-code, 157 | import-error 158 | 159 | # Enable the message, report, category or checker with the given id(s). You can 160 | # either give multiple identifier separated by comma (,) or put this option 161 | # multiple time (only on the command line, not in the configuration file where 162 | # it should appear only once). See also the "--disable" option for examples. 163 | enable=c-extension-no-member 164 | 165 | 166 | [LOGGING] 167 | 168 | # The type of string formatting that logging methods do. `old` means using % 169 | # formatting, `new` is for `{}` formatting. 170 | logging-format-style=old 171 | 172 | # Logging modules to check that the string format arguments are in logging 173 | # function parameter format. 174 | logging-modules=logging 175 | 176 | 177 | [SPELLING] 178 | 179 | # Limits count of emitted suggestions for spelling mistakes. 180 | max-spelling-suggestions=4 181 | 182 | # Spelling dictionary name. Available dictionaries: none. To make it work, 183 | # install the 'python-enchant' package. 184 | spelling-dict= 185 | 186 | # List of comma separated words that should be considered directives if they 187 | # appear at the beginning of a comment and should not be checked. 188 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 189 | 190 | # List of comma separated words that should not be checked. 191 | spelling-ignore-words= 192 | 193 | # A path to a file that contains the private dictionary; one word per line. 194 | spelling-private-dict-file= 195 | 196 | # Tells whether to store unknown words to the private dictionary (see the 197 | # --spelling-private-dict-file option) instead of raising a message. 198 | spelling-store-unknown-words=no 199 | 200 | 201 | [MISCELLANEOUS] 202 | 203 | # List of note tags to take in consideration, separated by a comma. 204 | notes=FIXME, 205 | XXX, 206 | TODO 207 | 208 | # Regular expression of note tags to take in consideration. 209 | notes-rgx= 210 | 211 | 212 | [TYPECHECK] 213 | 214 | # List of decorators that produce context managers, such as 215 | # contextlib.contextmanager. Add to this list to register other decorators that 216 | # produce valid context managers. 217 | contextmanager-decorators=contextlib.contextmanager 218 | 219 | # List of members which are set dynamically and missed by pylint inference 220 | # system, and so shouldn't trigger E1101 when accessed. Python regular 221 | # expressions are accepted. 222 | generated-members= 223 | 224 | # Tells whether to warn about missing members when the owner of the attribute 225 | # is inferred to be None. 226 | ignore-none=yes 227 | 228 | # This flag controls whether pylint should warn about no-member and similar 229 | # checks whenever an opaque object is returned when inferring. The inference 230 | # can return multiple potential results while evaluating a Python object, but 231 | # some branches might not be evaluated, which results in partial inference. In 232 | # that case, it might be useful to still emit no-member and other checks for 233 | # the rest of the inferred objects. 234 | ignore-on-opaque-inference=yes 235 | 236 | # List of symbolic message names to ignore for Mixin members. 237 | ignored-checks-for-mixins=no-member, 238 | not-async-context-manager, 239 | not-context-manager, 240 | attribute-defined-outside-init 241 | 242 | # List of class names for which member attributes should not be checked (useful 243 | # for classes with dynamically set attributes). This supports the use of 244 | # qualified names. 245 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 246 | 247 | # Show a hint with possible names when a member name was not found. The aspect 248 | # of finding the hint is based on edit distance. 249 | missing-member-hint=yes 250 | 251 | # The minimum edit distance a name should have in order to be considered a 252 | # similar match for a missing member name. 253 | missing-member-hint-distance=1 254 | 255 | # The total number of similar names that should be taken in consideration when 256 | # showing a hint for a missing member. 257 | missing-member-max-choices=1 258 | 259 | # Regex pattern to define which classes are considered mixins. 260 | mixin-class-rgx=.*[Mm]ixin 261 | 262 | # List of decorators that change the signature of a decorated function. 263 | signature-mutators= 264 | 265 | 266 | [CLASSES] 267 | 268 | # Warn about protected attribute access inside special methods 269 | check-protected-access-in-special-methods=no 270 | 271 | # List of method names used to declare (i.e. assign) instance attributes. 272 | defining-attr-methods=__init__, 273 | __new__, 274 | setUp, 275 | __post_init__ 276 | 277 | # List of member names, which should be excluded from the protected access 278 | # warning. 279 | exclude-protected=_asdict, 280 | _fields, 281 | _replace, 282 | _source, 283 | _make 284 | 285 | # List of valid names for the first argument in a class method. 286 | valid-classmethod-first-arg=cls 287 | 288 | # List of valid names for the first argument in a metaclass class method. 289 | valid-metaclass-classmethod-first-arg=cls 290 | 291 | 292 | [VARIABLES] 293 | 294 | # List of additional names supposed to be defined in builtins. Remember that 295 | # you should avoid defining new builtins when possible. 296 | additional-builtins= 297 | 298 | # Tells whether unused global variables should be treated as a violation. 299 | allow-global-unused-variables=yes 300 | 301 | # List of names allowed to shadow builtins 302 | allowed-redefined-builtins= 303 | 304 | # List of strings which can identify a callback function by name. A callback 305 | # name must start or end with one of those strings. 306 | callbacks=cb_, 307 | _cb 308 | 309 | # A regular expression matching the name of dummy variables (i.e. expected to 310 | # not be used). 311 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 312 | 313 | # Argument names that match this expression will be ignored. Default to name 314 | # with leading underscore. 315 | ignored-argument-names=_.*|^ignored_|^unused_ 316 | 317 | # Tells whether we should check for unused import in __init__ files. 318 | init-import=no 319 | 320 | # List of qualified module names which can have objects that can redefine 321 | # builtins. 322 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 323 | 324 | 325 | [FORMAT] 326 | 327 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 328 | expected-line-ending-format= 329 | 330 | # Regexp for a line that is allowed to be longer than the limit. 331 | ignore-long-lines=^\s*(# )??$ 332 | 333 | # Number of spaces of indent required inside a hanging or continued line. 334 | indent-after-paren=4 335 | 336 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 337 | # tab). 338 | indent-string=' ' 339 | 340 | # Maximum number of characters on a single line. 341 | max-line-length=100 342 | 343 | # Maximum number of lines in a module. 344 | max-module-lines=1000 345 | 346 | # Allow the body of a class to be on the same line as the declaration if body 347 | # contains single statement. 348 | single-line-class-stmt=no 349 | 350 | # Allow the body of an if to be on the same line as the test if there is no 351 | # else. 352 | single-line-if-stmt=no 353 | 354 | 355 | [IMPORTS] 356 | 357 | # List of modules that can be imported at any level, not just the top level 358 | # one. 359 | allow-any-import-level= 360 | 361 | # Allow wildcard imports from modules that define __all__. 362 | allow-wildcard-with-all=no 363 | 364 | # Deprecated modules which should not be used, separated by a comma. 365 | deprecated-modules= 366 | 367 | # Output a graph (.gv or any supported image format) of external dependencies 368 | # to the given file (report RP0402 must not be disabled). 369 | ext-import-graph= 370 | 371 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 372 | # external) dependencies to the given file (report RP0402 must not be 373 | # disabled). 374 | import-graph= 375 | 376 | # Output a graph (.gv or any supported image format) of internal dependencies 377 | # to the given file (report RP0402 must not be disabled). 378 | int-import-graph= 379 | 380 | # Force import order to recognize a module as part of the standard 381 | # compatibility libraries. 382 | known-standard-library= 383 | 384 | # Force import order to recognize a module as part of a third party library. 385 | known-third-party=enchant 386 | 387 | # Couples of modules and preferred modules, separated by a comma. 388 | preferred-modules= 389 | 390 | 391 | [EXCEPTIONS] 392 | 393 | # Exceptions that will emit a warning when caught. 394 | overgeneral-exceptions=BaseException, 395 | Exception 396 | 397 | 398 | [REFACTORING] 399 | 400 | # Maximum number of nested blocks for function / method body 401 | max-nested-blocks=5 402 | 403 | # Complete name of functions that never returns. When checking for 404 | # inconsistent-return-statements if a never returning function is called then 405 | # it will be considered as an explicit return statement and no message will be 406 | # printed. 407 | never-returning-functions=sys.exit,argparse.parse_error 408 | 409 | 410 | [SIMILARITIES] 411 | 412 | # Comments are removed from the similarity computation 413 | ignore-comments=yes 414 | 415 | # Docstrings are removed from the similarity computation 416 | ignore-docstrings=yes 417 | 418 | # Imports are removed from the similarity computation 419 | ignore-imports=yes 420 | 421 | # Signatures are removed from the similarity computation 422 | ignore-signatures=yes 423 | 424 | # Minimum lines number of a similarity. 425 | min-similarity-lines=4 426 | 427 | 428 | [DESIGN] 429 | 430 | # List of regular expressions of class ancestor names to ignore when counting 431 | # public methods (see R0903) 432 | exclude-too-few-public-methods= 433 | 434 | # List of qualified class names to ignore when counting class parents (see 435 | # R0901) 436 | ignored-parents= 437 | 438 | # Maximum number of arguments for function / method. 439 | max-args=5 440 | 441 | # Maximum number of attributes for a class (see R0902). 442 | max-attributes=7 443 | 444 | # Maximum number of boolean expressions in an if statement (see R0916). 445 | max-bool-expr=5 446 | 447 | # Maximum number of branch for function / method body. 448 | max-branches=12 449 | 450 | # Maximum number of locals for function / method body. 451 | max-locals=15 452 | 453 | # Maximum number of parents for a class (see R0901). 454 | max-parents=7 455 | 456 | # Maximum number of public methods for a class (see R0904). 457 | max-public-methods=20 458 | 459 | # Maximum number of return / yield for function / method body. 460 | max-returns=6 461 | 462 | # Maximum number of statements in function / method body. 463 | max-statements=50 464 | 465 | # Minimum number of public methods for a class (see R0903). 466 | min-public-methods=2 467 | 468 | 469 | [STRING] 470 | 471 | # This flag controls whether inconsistent-quotes generates a warning when the 472 | # character used as a quote delimiter is used inconsistently within a module. 473 | check-quote-consistency=no 474 | 475 | # This flag controls whether the implicit-str-concat should generate a warning 476 | # on implicit string concatenation in sequences defined over several lines. 477 | check-str-concat-over-line-jumps=no 478 | 479 | 480 | [BASIC] 481 | 482 | # Naming style matching correct argument names. 483 | argument-naming-style=snake_case 484 | 485 | # Regular expression matching correct argument names. Overrides argument- 486 | # naming-style. If left empty, argument names will be checked with the set 487 | # naming style. 488 | #argument-rgx= 489 | 490 | # Naming style matching correct attribute names. 491 | attr-naming-style=snake_case 492 | 493 | # Regular expression matching correct attribute names. Overrides attr-naming- 494 | # style. If left empty, attribute names will be checked with the set naming 495 | # style. 496 | #attr-rgx= 497 | 498 | # Bad variable names which should always be refused, separated by a comma. 499 | bad-names=foo, 500 | bar, 501 | baz, 502 | toto, 503 | tutu, 504 | tata 505 | 506 | # Bad variable names regexes, separated by a comma. If names match any regex, 507 | # they will always be refused 508 | bad-names-rgxs= 509 | 510 | # Naming style matching correct class attribute names. 511 | class-attribute-naming-style=any 512 | 513 | # Regular expression matching correct class attribute names. Overrides class- 514 | # attribute-naming-style. If left empty, class attribute names will be checked 515 | # with the set naming style. 516 | #class-attribute-rgx= 517 | 518 | # Naming style matching correct class constant names. 519 | class-const-naming-style=UPPER_CASE 520 | 521 | # Regular expression matching correct class constant names. Overrides class- 522 | # const-naming-style. If left empty, class constant names will be checked with 523 | # the set naming style. 524 | #class-const-rgx= 525 | 526 | # Naming style matching correct class names. 527 | class-naming-style=PascalCase 528 | 529 | # Regular expression matching correct class names. Overrides class-naming- 530 | # style. If left empty, class names will be checked with the set naming style. 531 | #class-rgx= 532 | 533 | # Naming style matching correct constant names. 534 | const-naming-style=UPPER_CASE 535 | 536 | # Regular expression matching correct constant names. Overrides const-naming- 537 | # style. If left empty, constant names will be checked with the set naming 538 | # style. 539 | #const-rgx= 540 | 541 | # Minimum line length for functions/classes that require docstrings, shorter 542 | # ones are exempt. 543 | docstring-min-length=-1 544 | 545 | # Naming style matching correct function names. 546 | function-naming-style=snake_case 547 | 548 | # Regular expression matching correct function names. Overrides function- 549 | # naming-style. If left empty, function names will be checked with the set 550 | # naming style. 551 | #function-rgx= 552 | 553 | # Good variable names which should always be accepted, separated by a comma. 554 | good-names=i, 555 | j, 556 | k, 557 | ex, 558 | Run, 559 | _ 560 | 561 | # Good variable names regexes, separated by a comma. If names match any regex, 562 | # they will always be accepted 563 | good-names-rgxs= 564 | 565 | # Include a hint for the correct naming format with invalid-name. 566 | include-naming-hint=no 567 | 568 | # Naming style matching correct inline iteration names. 569 | inlinevar-naming-style=any 570 | 571 | # Regular expression matching correct inline iteration names. Overrides 572 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 573 | # with the set naming style. 574 | #inlinevar-rgx= 575 | 576 | # Naming style matching correct method names. 577 | method-naming-style=snake_case 578 | 579 | # Regular expression matching correct method names. Overrides method-naming- 580 | # style. If left empty, method names will be checked with the set naming style. 581 | #method-rgx= 582 | 583 | # Naming style matching correct module names. 584 | module-naming-style=snake_case 585 | 586 | # Regular expression matching correct module names. Overrides module-naming- 587 | # style. If left empty, module names will be checked with the set naming style. 588 | #module-rgx= 589 | 590 | # Colon-delimited sets of names that determine each other's naming style when 591 | # the name regexes allow several styles. 592 | name-group= 593 | 594 | # Regular expression which should only match function or class names that do 595 | # not require a docstring. 596 | no-docstring-rgx=^_ 597 | 598 | # List of decorators that produce properties, such as abc.abstractproperty. Add 599 | # to this list to register other decorators that produce valid properties. 600 | # These decorators are taken in consideration only for invalid-name. 601 | property-classes=abc.abstractproperty 602 | 603 | # Regular expression matching correct type variable names. If left empty, type 604 | # variable names will be checked with the set naming style. 605 | #typevar-rgx= 606 | 607 | # Naming style matching correct variable names. 608 | variable-naming-style=snake_case 609 | 610 | # Regular expression matching correct variable names. Overrides variable- 611 | # naming-style. If left empty, variable names will be checked with the set 612 | # naming style. 613 | #variable-rgx= 614 | -------------------------------------------------------------------------------- /.secretlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": [ 3 | { 4 | "id": "@secretlint/secretlint-rule-preset-recommend" 5 | }, 6 | { 7 | "id": "@secretlint/secretlint-rule-basicauth" 8 | }, 9 | { 10 | "id": "@secretlint/secretlint-rule-pattern", 11 | "options": { 12 | "patterns": [ 13 | { 14 | "name": "password=", 15 | "pattern": "password\\s*=\\s*(?[\\w\\d!@#$%^&(){}\\[\\]:\";'<>,.?\/~`_+-=|]{1,256})\\b.*" 16 | } 17 | ] 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | }, 15 | { 16 | "name": "Python: Debug Tests", 17 | "type": "python", 18 | "request": "launch", 19 | "program": "${file}", 20 | "purpose": ["debug-test"], 21 | "console": "integratedTerminal", 22 | "justMyCode": false 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [], 3 | "python.testing.unittestEnabled": false, 4 | "python.testing.pytestEnabled": true 5 | } -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | ignore: | 2 | azure-*.yml 3 | rules: 4 | braces: 5 | level: error 6 | min-spaces-inside: 0 7 | max-spaces-inside: 1 8 | min-spaces-inside-empty: -1 9 | max-spaces-inside-empty: -1 10 | brackets: 11 | level: error 12 | min-spaces-inside: 0 13 | max-spaces-inside: 0 14 | min-spaces-inside-empty: -1 15 | max-spaces-inside-empty: -1 16 | colons: 17 | level: error 18 | max-spaces-before: 0 19 | max-spaces-after: 1 20 | commas: 21 | level: error 22 | max-spaces-before: 0 23 | min-spaces-after: 1 24 | max-spaces-after: 1 25 | comments: 26 | level: error 27 | require-starting-space: true 28 | min-spaces-from-content: 2 29 | comments-indentation: 30 | level: error 31 | document-end: 32 | level: error 33 | present: false 34 | document-start: 35 | level: error 36 | present: false 37 | empty-lines: 38 | level: error 39 | max: 1 40 | max-start: 0 41 | max-end: 1 42 | hyphens: 43 | level: error 44 | max-spaces-after: 1 45 | indentation: 46 | level: error 47 | spaces: 2 48 | indent-sequences: true 49 | check-multi-line-strings: false 50 | key-duplicates: 51 | level: error 52 | line-length: disable 53 | new-line-at-end-of-file: 54 | level: error 55 | new-lines: 56 | level: error 57 | type: unix 58 | trailing-spaces: 59 | level: error 60 | truthy: disable 61 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pyhive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include pyhiveapi * 2 | include requirements.txt 3 | include requirements_test.txt 4 | recursive-include data * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![CodeQL](https://github.com/Pyhive/Pyhiveapi/workflows/CodeQL/badge.svg) ![Python Linting](https://github.com/Pyhive/Pyhiveapi/workflows/Python%20package/badge.svg) 3 | 4 | # Introduction 5 | This is a library which intefaces with the Hive smart home platform. 6 | This library is built mainly to integrate with the Home Assistant platform, 7 | but it can also be used independently (See examples below.) 8 | 9 | 10 | ## Examples 11 | Here are examples and documentation on how to use the library independently. 12 | 13 | https://pyhass.github.io/pyhiveapi.docs/ [WIP] 14 | 15 | 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 0.3.x | :white_check_mark: | 11 | | < 0.3 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Use this section to tell people how to report a vulnerability. 16 | 17 | Tell them where to go, how often they can expect to get an update on a 18 | reported vulnerability, what to expect if the vulnerability is accepted or 19 | declined, etc. 20 | -------------------------------------------------------------------------------- /dev.env: -------------------------------------------------------------------------------- 1 | PYTHONASYNCIODEBUG=1 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "husky": { 3 | "hooks": { 4 | "pre-commit": "lint-staged" 5 | } 6 | }, 7 | "lint-staged": { 8 | "*": [ 9 | "secretlint" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/__init__.py: -------------------------------------------------------------------------------- 1 | """__init__.py.""" 2 | # pylint: skip-file 3 | if __name__ == "pyhiveapi": 4 | from .api.hive_api import HiveApi as API # noqa: F401 5 | from .api.hive_auth import HiveAuth as Auth # noqa: F401 6 | else: 7 | from .api.hive_async_api import HiveApiAsync as API # noqa: F401 8 | from .api.hive_auth_async import HiveAuthAsync as Auth # noqa: F401 9 | 10 | from .helper.const import SMS_REQUIRED # noqa: F401 11 | from .hive import Hive # noqa: F401 12 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/action.py: -------------------------------------------------------------------------------- 1 | """Hive Action Module.""" 2 | # pylint: skip-file 3 | 4 | 5 | class HiveAction: 6 | """Hive Action Code. 7 | 8 | Returns: 9 | object: Return hive action object. 10 | """ 11 | 12 | actionType = "Actions" 13 | 14 | def __init__(self, session: object = None): 15 | """Initialise Action. 16 | 17 | Args: 18 | session (object, optional): session to interact with hive account. Defaults to None. 19 | """ 20 | self.session = session 21 | 22 | async def getAction(self, device: dict): 23 | """Action device to update. 24 | 25 | Args: 26 | device (dict): Device to be updated. 27 | 28 | Returns: 29 | dict: Updated device. 30 | """ 31 | dev_data = {} 32 | 33 | if device["hiveID"] in self.data["action"]: 34 | dev_data = { 35 | "hiveID": device["hiveID"], 36 | "hiveName": device["hiveName"], 37 | "hiveType": device["hiveType"], 38 | "haName": device["haName"], 39 | "haType": device["haType"], 40 | "status": {"state": await self.getState(device)}, 41 | "power_usage": None, 42 | "deviceData": {}, 43 | "custom": device.get("custom", None), 44 | } 45 | 46 | self.session.devices.update({device["hiveID"]: dev_data}) 47 | return self.session.devices[device["hiveID"]] 48 | else: 49 | exists = self.session.data.actions.get("hiveID", False) 50 | if exists is False: 51 | return "REMOVE" 52 | return device 53 | 54 | async def getState(self, device: dict): 55 | """Get action state. 56 | 57 | Args: 58 | device (dict): Device to get state of. 59 | 60 | Returns: 61 | str: Return state. 62 | """ 63 | final = None 64 | 65 | try: 66 | data = self.session.data.actions[device["hiveID"]] 67 | final = data["enabled"] 68 | except KeyError as e: 69 | await self.session.log.error(e) 70 | 71 | return final 72 | 73 | async def setStatusOn(self, device: dict): 74 | """Set action turn on. 75 | 76 | Args: 77 | device (dict): Device to set state of. 78 | 79 | Returns: 80 | boolean: True/False if successful. 81 | """ 82 | import json 83 | 84 | final = False 85 | 86 | if device["hiveID"] in self.session.data.actions: 87 | await self.session.hiveRefreshTokens() 88 | data = self.session.data.actions[device["hiveID"]] 89 | data.update({"enabled": True}) 90 | send = json.dumps(data) 91 | resp = await self.session.api.setAction(device["hiveID"], send) 92 | if resp["original"] == 200: 93 | final = True 94 | await self.session.getDevices(device["hiveID"]) 95 | 96 | return final 97 | 98 | async def setStatusOff(self, device: dict): 99 | """Set action to turn off. 100 | 101 | Args: 102 | device (dict): Device to set state of. 103 | 104 | Returns: 105 | boolean: True/False if successful. 106 | """ 107 | import json 108 | 109 | final = False 110 | 111 | if device["hiveID"] in self.session.data.actions: 112 | await self.session.hiveRefreshTokens() 113 | data = self.session.data.actions[device["hiveID"]] 114 | data.update({"enabled": False}) 115 | send = json.dumps(data) 116 | resp = await self.session.api.setAction(device["hiveID"], send) 117 | if resp["original"] == 200: 118 | final = True 119 | await self.session.getDevices(device["hiveID"]) 120 | 121 | return final 122 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/alarm.py: -------------------------------------------------------------------------------- 1 | """Hive Alarm Module.""" 2 | # pylint: skip-file 3 | 4 | 5 | class HiveHomeShield: 6 | """Hive homeshield alarm. 7 | 8 | Returns: 9 | object: Hive homeshield 10 | """ 11 | 12 | alarmType = "Alarm" 13 | 14 | async def getMode(self): 15 | """Get current mode of the alarm. 16 | 17 | Returns: 18 | str: Mode if the alarm [armed_home, armed_away, armed_night] 19 | """ 20 | state = None 21 | 22 | try: 23 | data = self.session.data.alarm 24 | state = data["mode"] 25 | except KeyError as e: 26 | await self.session.log.error(e) 27 | 28 | return state 29 | 30 | async def getState(self, device: dict): 31 | """Get the alarm triggered state. 32 | 33 | Returns: 34 | boolean: True/False if alarm is triggered. 35 | """ 36 | state = None 37 | 38 | try: 39 | data = self.session.data.devices[device["hiveID"]] 40 | state = data["state"]["alarmActive"] 41 | except KeyError as e: 42 | await self.session.log.error(e) 43 | 44 | return state 45 | 46 | async def setMode(self, device: dict, mode: str): 47 | """Set the alarm mode. 48 | 49 | Args: 50 | device (dict): Alarm device. 51 | 52 | Returns: 53 | boolean: True/False if successful. 54 | """ 55 | final = False 56 | 57 | if ( 58 | device["hiveID"] in self.session.data.devices 59 | and device["deviceData"]["online"] 60 | ): 61 | await self.session.hiveRefreshTokens() 62 | resp = await self.session.api.setAlarm(mode=mode) 63 | if resp["original"] == 200: 64 | final = True 65 | await self.session.getAlarm() 66 | 67 | return final 68 | 69 | 70 | class Alarm(HiveHomeShield): 71 | """Home assistant alarm. 72 | 73 | Args: 74 | HiveHomeShield (object): Class object. 75 | """ 76 | 77 | def __init__(self, session: object = None): 78 | """Initialise alarm. 79 | 80 | Args: 81 | session (object, optional): Used to interact with the hive account. Defaults to None. 82 | """ 83 | self.session = session 84 | 85 | async def getAlarm(self, device: dict): 86 | """Get alarm data. 87 | 88 | Args: 89 | device (dict): Device to update. 90 | 91 | Returns: 92 | dict: Updated device. 93 | """ 94 | device["deviceData"].update( 95 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 96 | ) 97 | dev_data = {} 98 | 99 | if device["deviceData"]["online"]: 100 | self.session.helper.deviceRecovered(device["device_id"]) 101 | data = self.session.data.devices[device["device_id"]] 102 | dev_data = { 103 | "hiveID": device["hiveID"], 104 | "hiveName": device["hiveName"], 105 | "hiveType": device["hiveType"], 106 | "haName": device["haName"], 107 | "haType": device["haType"], 108 | "device_id": device["device_id"], 109 | "device_name": device["device_name"], 110 | "status": { 111 | "state": await self.getState(device), 112 | "mode": await self.getMode(), 113 | }, 114 | "deviceData": data.get("props", None), 115 | "parentDevice": data.get("parent", None), 116 | "custom": device.get("custom", None), 117 | "attributes": await self.session.attr.stateAttributes( 118 | device["device_id"], device["hiveType"] 119 | ), 120 | } 121 | 122 | self.session.devices.update({device["hiveID"]: dev_data}) 123 | return self.session.devices[device["hiveID"]] 124 | else: 125 | await self.session.log.errorCheck( 126 | device["device_id"], "ERROR", device["deviceData"]["online"] 127 | ) 128 | return device 129 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/api/__init__.py: -------------------------------------------------------------------------------- 1 | """__init__.py file.""" 2 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/api/hive_api.py: -------------------------------------------------------------------------------- 1 | """Hive API Module.""" 2 | # pylint: skip-file 3 | import json 4 | 5 | import requests 6 | import urllib3 7 | from pyquery import PyQuery 8 | 9 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 10 | 11 | 12 | class HiveApi: 13 | """Hive API Code.""" 14 | 15 | def __init__(self, hiveSession=None, websession=None, token=None): 16 | """Hive API initialisation.""" 17 | self.cameraBaseUrl = "prod.hcam.bgchtest.info" 18 | self.urls = { 19 | "properties": "https://sso.hivehome.com/", 20 | "login": "https://beekeeper.hivehome.com/1.0/cognito/login", 21 | "refresh": "https://beekeeper.hivehome.com/1.0/cognito/refresh-token", 22 | "long_lived": "https://api.prod.bgchprod.info/omnia/accessTokens", 23 | "base": "https://beekeeper-uk.hivehome.com/1.0", 24 | "weather": "https://weather.prod.bgchprod.info/weather", 25 | "holiday_mode": "/holiday-mode", 26 | "all": "/nodes/all?products=true&devices=true&actions=true", 27 | "alarm": "/security-lite?homeId=", 28 | "cameraImages": f"https://event-history-service.{self.cameraBaseUrl}/v1/events/cameras?latest=true&cameraId={{0}}", 29 | "cameraRecordings": f"https://event-history-service.{self.cameraBaseUrl}/v1/playlist/cameras/{{0}}/events/{{1}}.m3u8", 30 | "devices": "/devices", 31 | "products": "/products", 32 | "actions": "/actions", 33 | "nodes": "/nodes/{0}/{1}", 34 | } 35 | self.timeout = 10 36 | self.json_return = { 37 | "original": "No response to Hive API request", 38 | "parsed": "No response to Hive API request", 39 | } 40 | self.session = hiveSession 41 | self.token = token 42 | 43 | def request(self, type, url, jsc=None, camera=False): 44 | """Make API request.""" 45 | if self.session is not None: 46 | if camera: 47 | self.headers = { 48 | "content-type": "application/json", 49 | "Accept": "*/*", 50 | "Authorization": f"Bearer {self.session.tokens.tokenData['token']}", 51 | "x-jwt-token": self.session.tokens.tokenData["token"], 52 | } 53 | else: 54 | self.headers = { 55 | "content-type": "application/json", 56 | "Accept": "*/*", 57 | "authorization": self.session.tokens.tokenData["token"], 58 | } 59 | else: 60 | if camera: 61 | self.headers = { 62 | "content-type": "application/json", 63 | "Accept": "*/*", 64 | "Authorization": f"Bearer {self.token}", 65 | "x-jwt-token": self.token, 66 | } 67 | else: 68 | self.headers = { 69 | "content-type": "application/json", 70 | "Accept": "*/*", 71 | "authorization": self.token, 72 | } 73 | 74 | if type == "GET": 75 | return requests.get( 76 | url=url, headers=self.headers, data=jsc, timeout=self.timeout 77 | ) 78 | if type == "POST": 79 | return requests.post( 80 | url=url, headers=self.headers, data=jsc, timeout=self.timeout 81 | ) 82 | 83 | def refreshTokens(self, tokens={}): 84 | """Get new session tokens - DEPRECATED NOW BY AWS TOKEN MANAGEMENT.""" 85 | url = self.urls["refresh"] 86 | if self.session is not None: 87 | tokens = self.session.tokens.tokenData 88 | jsc = ( 89 | "{" 90 | + ",".join( 91 | ('"' + str(i) + '": ' '"' + str(t) + '" ' for i, t in tokens.items()) 92 | ) 93 | + "}" 94 | ) 95 | try: 96 | info = self.request("POST", url, jsc) 97 | data = json.loads(info.text) 98 | if "token" in data and self.session: 99 | self.session.updateTokens(data) 100 | self.urls.update({"base": data["platform"]["endpoint"]}) 101 | self.urls.update({"camera": data["platform"]["cameraPlatform"]}) 102 | self.json_return.update({"original": info.status_code}) 103 | self.json_return.update({"parsed": info.json()}) 104 | except (OSError, RuntimeError, ZeroDivisionError): 105 | self.error() 106 | 107 | return self.json_return 108 | 109 | def getLoginInfo(self): 110 | """Get login properties to make the login request.""" 111 | url = self.urls["properties"] 112 | try: 113 | data = requests.get(url=url, verify=False, timeout=self.timeout) 114 | html = PyQuery(data.content) 115 | json_data = json.loads( 116 | '{"' 117 | + (html("script:first").text()) 118 | .replace(",", ', "') 119 | .replace("=", '":') 120 | .replace("window.", "") 121 | + "}" 122 | ) 123 | 124 | loginData = {} 125 | loginData.update({"UPID": json_data["HiveSSOPoolId"]}) 126 | loginData.update({"CLIID": json_data["HiveSSOPublicCognitoClientId"]}) 127 | loginData.update({"REGION": json_data["HiveSSOPoolId"]}) 128 | return loginData 129 | except (OSError, RuntimeError, ZeroDivisionError): 130 | self.error() 131 | 132 | def getAll(self): 133 | """Build and query all endpoint.""" 134 | json_return = {} 135 | url = self.urls["base"] + self.urls["all"] 136 | try: 137 | info = self.request("GET", url) 138 | json_return.update({"original": info.status_code}) 139 | json_return.update({"parsed": info.json()}) 140 | except (OSError, RuntimeError, ZeroDivisionError): 141 | self.error() 142 | 143 | return json_return 144 | 145 | def getAlarm(self, homeID=None): 146 | """Build and query alarm endpoint.""" 147 | if self.session is not None: 148 | homeID = self.session.config.homeID 149 | url = self.urls["base"] + self.urls["alarm"] + homeID 150 | try: 151 | info = self.request("GET", url) 152 | self.json_return.update({"original": info.status_code}) 153 | self.json_return.update({"parsed": info.json()}) 154 | except (OSError, RuntimeError, ZeroDivisionError): 155 | self.error() 156 | 157 | return self.json_return 158 | 159 | def getCameraImage(self, device=None, accessToken=None): 160 | """Build and query camera endpoint.""" 161 | json_return = {} 162 | url = self.urls["cameraImages"].format(device["props"]["hardwareIdentifier"]) 163 | try: 164 | info = self.request("GET", url, camera=True) 165 | json_return.update({"original": info.status_code}) 166 | json_return.update({"parsed": info.json()}) 167 | except (OSError, RuntimeError, ZeroDivisionError): 168 | self.error() 169 | 170 | return json_return 171 | 172 | def getCameraRecording(self, device=None, eventId=None): 173 | """Build and query camera endpoint.""" 174 | json_return = {} 175 | url = self.urls["cameraRecordings"].format( 176 | device["props"]["hardwareIdentifier"], eventId 177 | ) 178 | try: 179 | info = self.request("GET", url, camera=True) 180 | json_return.update({"original": info.status_code}) 181 | json_return.update({"parsed": info.text.split("\n")[3]}) 182 | except (OSError, RuntimeError, ZeroDivisionError): 183 | self.error() 184 | 185 | return json_return 186 | 187 | def getDevices(self): 188 | """Call the get devices endpoint.""" 189 | url = self.urls["base"] + self.urls["devices"] 190 | try: 191 | response = self.request("GET", url) 192 | self.json_return.update({"original": response.status_code}) 193 | self.json_return.update({"parsed": response.json()}) 194 | except (OSError, RuntimeError, ZeroDivisionError): 195 | self.error() 196 | 197 | return self.json_return 198 | 199 | def getProducts(self): 200 | """Call the get products endpoint.""" 201 | url = self.urls["base"] + self.urls["products"] 202 | try: 203 | response = self.request("GET", url) 204 | self.json_return.update({"original": response.status_code}) 205 | self.json_return.update({"parsed": response.json()}) 206 | except (OSError, RuntimeError, ZeroDivisionError): 207 | self.error() 208 | 209 | return self.json_return 210 | 211 | def getActions(self): 212 | """Call the get actions endpoint.""" 213 | url = self.urls["base"] + self.urls["actions"] 214 | try: 215 | response = self.request("GET", url) 216 | self.json_return.update({"original": response.status_code}) 217 | self.json_return.update({"parsed": response.json()}) 218 | except (OSError, RuntimeError, ZeroDivisionError): 219 | self.error() 220 | 221 | return self.json_return 222 | 223 | def motionSensor(self, sensor, fromepoch, toepoch): 224 | """Call a way to get motion sensor info.""" 225 | url = ( 226 | self.urls["base"] 227 | + self.urls["products"] 228 | + "/" 229 | + sensor["type"] 230 | + "/" 231 | + sensor["id"] 232 | + "/events?from=" 233 | + str(fromepoch) 234 | + "&to=" 235 | + str(toepoch) 236 | ) 237 | try: 238 | response = self.request("GET", url) 239 | self.json_return.update({"original": response.status_code}) 240 | self.json_return.update({"parsed": response.json()}) 241 | except (OSError, RuntimeError, ZeroDivisionError): 242 | self.error() 243 | 244 | return self.json_return 245 | 246 | def getWeather(self, weather_url): 247 | """Call endpoint to get local weather from Hive API.""" 248 | t_url = self.urls["weather"] + weather_url 249 | url = t_url.replace(" ", "%20") 250 | try: 251 | response = self.request("GET", url) 252 | self.json_return.update({"original": response.status_code}) 253 | self.json_return.update({"parsed": response.json()}) 254 | except (OSError, RuntimeError, ZeroDivisionError, ConnectionError): 255 | self.error() 256 | 257 | return self.json_return 258 | 259 | def setState(self, n_type, n_id, **kwargs): 260 | """Set the state of a Device.""" 261 | jsc = ( 262 | "{" 263 | + ",".join( 264 | ('"' + str(i) + '": ' '"' + str(t) + '" ' for i, t in kwargs.items()) 265 | ) 266 | + "}" 267 | ) 268 | 269 | url = self.urls["base"] + self.urls["nodes"].format(n_type, n_id) 270 | 271 | try: 272 | response = self.request("POST", url, jsc) 273 | self.json_return.update({"original": response.status_code}) 274 | self.json_return.update({"parsed": response.json()}) 275 | except (OSError, RuntimeError, ZeroDivisionError, ConnectionError): 276 | self.error() 277 | 278 | return self.json_return 279 | 280 | def setAction(self, n_id, data): 281 | """Set the state of a Action.""" 282 | jsc = data 283 | url = self.urls["base"] + self.urls["actions"] + "/" + n_id 284 | try: 285 | response = self.request("POST", url, jsc) 286 | self.json_return.update({"original": response.status_code}) 287 | self.json_return.update({"parsed": response.json()}) 288 | except (OSError, RuntimeError, ZeroDivisionError, ConnectionError): 289 | self.error() 290 | 291 | return self.json_return 292 | 293 | def error(self): 294 | """An error has occurred interacting with the Hive API.""" 295 | self.json_return.update({"original": "Error making API call"}) 296 | self.json_return.update({"parsed": "Error making API call"}) 297 | 298 | 299 | class UnknownConfig(Exception): 300 | """Unknown API config.""" 301 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/api/hive_async_api.py: -------------------------------------------------------------------------------- 1 | """Hive API Module.""" 2 | # pylint: skip-file 3 | import json 4 | from typing import Optional 5 | 6 | import requests 7 | import urllib3 8 | from aiohttp import ClientResponse, ClientSession, web_exceptions 9 | from pyquery import PyQuery 10 | 11 | from ..helper.const import HTTP_UNAUTHORIZED 12 | from ..helper.hive_exceptions import FileInUse, HiveApiError, NoApiToken 13 | 14 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 15 | 16 | 17 | class HiveApiAsync: 18 | """Hive API Code.""" 19 | 20 | def __init__(self, hiveSession=None, websession: Optional[ClientSession] = None): 21 | """Hive API initialisation.""" 22 | self.baseUrl = "https://beekeeper.hivehome.com/1.0" 23 | self.cameraBaseUrl = "prod.hcam.bgchtest.info" 24 | self.urls = { 25 | "properties": "https://sso.hivehome.com/", 26 | "login": f"{self.baseUrl}/cognito/login", 27 | "refresh": f"{self.baseUrl}/cognito/refresh-token", 28 | "holiday_mode": f"{self.baseUrl}/holiday-mode", 29 | "all": f"{self.baseUrl}/nodes/all?products=true&devices=true&actions=true", 30 | "alarm": f"{self.baseUrl}/security-lite?homeId=", 31 | "cameraImages": f"https://event-history-service.{self.cameraBaseUrl}/v1/events/cameras?latest=true&cameraId={{0}}", 32 | "cameraRecordings": f"https://event-history-service.{self.cameraBaseUrl}/v1/playlist/cameras/{{0}}/events/{{1}}.m3u8", 33 | "devices": f"{self.baseUrl}/devices", 34 | "products": f"{self.baseUrl}/products", 35 | "actions": f"{self.baseUrl}/actions", 36 | "nodes": f"{self.baseUrl}/nodes/{{0}}/{{1}}", 37 | "long_lived": "https://api.prod.bgchprod.info/omnia/accessTokens", 38 | "weather": "https://weather.prod.bgchprod.info/weather", 39 | } 40 | self.timeout = 10 41 | self.json_return = { 42 | "original": "No response to Hive API request", 43 | "parsed": "No response to Hive API request", 44 | } 45 | self.session = hiveSession 46 | self.websession = ClientSession() if websession is None else websession 47 | 48 | async def request( 49 | self, method: str, url: str, camera: bool = False, **kwargs 50 | ) -> ClientResponse: 51 | """Make a request.""" 52 | data = kwargs.get("data", None) 53 | 54 | try: 55 | if camera: 56 | headers = { 57 | "content-type": "application/json", 58 | "Accept": "*/*", 59 | "Authorization": f"Bearer {self.session.tokens.tokenData['token']}", 60 | "x-jwt-token": self.session.tokens.tokenData["token"], 61 | "User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple", 62 | } 63 | else: 64 | headers = { 65 | "content-type": "application/json", 66 | "Accept": "*/*", 67 | "Authorization": self.session.tokens.tokenData["token"], 68 | "User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple", 69 | } 70 | except KeyError: 71 | if "sso" in url: 72 | pass 73 | else: 74 | raise NoApiToken 75 | 76 | async with self.websession.request( 77 | method, url, headers=headers, data=data 78 | ) as resp: 79 | await resp.text() 80 | if str(resp.status).startswith("20"): 81 | return resp 82 | 83 | if resp.status == HTTP_UNAUTHORIZED: 84 | self.session.logger.error( 85 | f"Hive token has expired when calling {url} - " 86 | f"HTTP status is - {resp.status}" 87 | ) 88 | elif url is not None and resp.status is not None: 89 | self.session.logger.error( 90 | f"Something has gone wrong calling {url} - " 91 | f"HTTP status is - {resp.status}" 92 | ) 93 | 94 | raise HiveApiError 95 | 96 | def getLoginInfo(self): 97 | """Get login properties to make the login request.""" 98 | url = "https://sso.hivehome.com/" 99 | 100 | data = requests.get(url=url, verify=False, timeout=self.timeout) 101 | html = PyQuery(data.content) 102 | json_data = json.loads( 103 | '{"' 104 | + (html("script:first").text()) 105 | .replace(",", ', "') 106 | .replace("=", '":') 107 | .replace("window.", "") 108 | + "}" 109 | ) 110 | 111 | loginData = {} 112 | loginData.update({"UPID": json_data["HiveSSOPoolId"]}) 113 | loginData.update({"CLIID": json_data["HiveSSOPublicCognitoClientId"]}) 114 | loginData.update({"REGION": json_data["HiveSSOPoolId"]}) 115 | return loginData 116 | 117 | async def refreshTokens(self): 118 | """Refresh tokens - DEPRECATED NOW BY AWS TOKEN MANAGEMENT.""" 119 | url = self.urls["refresh"] 120 | if self.session is not None: 121 | tokens = self.session.tokens.tokenData 122 | jsc = ( 123 | "{" 124 | + ",".join( 125 | ('"' + str(i) + '": ' '"' + str(t) + '" ' for i, t in tokens.items()) 126 | ) 127 | + "}" 128 | ) 129 | try: 130 | await self.request("post", url, data=jsc) 131 | 132 | if self.json_return["original"] == 200: 133 | info = self.json_return["parsed"] 134 | if "token" in info: 135 | await self.session.updateTokens(info) 136 | self.baseUrl = info["platform"]["endpoint"] 137 | self.cameraBaseUrl = info["platform"]["cameraPlatform"] 138 | return True 139 | except (ConnectionError, OSError, RuntimeError, ZeroDivisionError): 140 | await self.error() 141 | 142 | return self.json_return 143 | 144 | async def getAll(self): 145 | """Build and query all endpoint.""" 146 | json_return = {} 147 | url = self.urls["all"] 148 | try: 149 | resp = await self.request("get", url) 150 | json_return.update({"original": resp.status}) 151 | json_return.update({"parsed": await resp.json(content_type=None)}) 152 | except (OSError, RuntimeError, ZeroDivisionError): 153 | await self.error() 154 | 155 | return json_return 156 | 157 | async def getAlarm(self): 158 | """Build and query alarm endpoint.""" 159 | json_return = {} 160 | url = self.urls["alarm"] + self.session.config.homeID 161 | try: 162 | resp = await self.request("get", url) 163 | json_return.update({"original": resp.status}) 164 | json_return.update({"parsed": await resp.json(content_type=None)}) 165 | except (OSError, RuntimeError, ZeroDivisionError): 166 | await self.error() 167 | 168 | return json_return 169 | 170 | async def getCameraImage(self, device): 171 | """Build and query alarm endpoint.""" 172 | json_return = {} 173 | url = self.urls["cameraImages"].format(device["props"]["hardwareIdentifier"]) 174 | try: 175 | resp = await self.request("get", url, True) 176 | json_return.update({"original": resp.status}) 177 | json_return.update({"parsed": await resp.json(content_type=None)}) 178 | except (OSError, RuntimeError, ZeroDivisionError): 179 | await self.error() 180 | 181 | return json_return 182 | 183 | async def getCameraRecording(self, device, eventId): 184 | """Build and query alarm endpoint.""" 185 | json_return = {} 186 | url = self.urls["cameraRecordings"].format( 187 | device["props"]["hardwareIdentifier"], eventId 188 | ) 189 | try: 190 | resp = await self.request("get", url, True) 191 | recUrl = await resp.text() 192 | json_return.update({"original": resp.status}) 193 | json_return.update({"parsed": recUrl.split("\n")[3]}) 194 | except (OSError, RuntimeError, ZeroDivisionError): 195 | await self.error() 196 | 197 | return json_return 198 | 199 | async def getDevices(self): 200 | """Call the get devices endpoint.""" 201 | json_return = {} 202 | url = self.urls["devices"] 203 | try: 204 | resp = await self.request("get", url) 205 | json_return.update({"original": resp.status}) 206 | json_return.update({"parsed": await resp.json(content_type=None)}) 207 | except (OSError, RuntimeError, ZeroDivisionError): 208 | await self.error() 209 | 210 | return json_return 211 | 212 | async def getProducts(self): 213 | """Call the get products endpoint.""" 214 | json_return = {} 215 | url = self.urls["products"] 216 | try: 217 | resp = await self.request("get", url) 218 | json_return.update({"original": resp.status}) 219 | json_return.update({"parsed": await resp.json(content_type=None)}) 220 | except (OSError, RuntimeError, ZeroDivisionError): 221 | await self.error() 222 | 223 | return json_return 224 | 225 | async def getActions(self): 226 | """Call the get actions endpoint.""" 227 | json_return = {} 228 | url = self.urls["actions"] 229 | try: 230 | resp = await self.request("get", url) 231 | json_return.update({"original": resp.status}) 232 | json_return.update({"parsed": await resp.json(content_type=None)}) 233 | except (OSError, RuntimeError, ZeroDivisionError): 234 | await self.error() 235 | 236 | return json_return 237 | 238 | async def motionSensor(self, sensor, fromepoch, toepoch): 239 | """Call a way to get motion sensor info.""" 240 | json_return = {} 241 | url = ( 242 | self.urls["base"] 243 | + self.urls["products"] 244 | + "/" 245 | + sensor["type"] 246 | + "/" 247 | + sensor["id"] 248 | + "/events?from=" 249 | + str(fromepoch) 250 | + "&to=" 251 | + str(toepoch) 252 | ) 253 | try: 254 | resp = await self.request("get", url) 255 | json_return.update({"original": resp.status}) 256 | json_return.update({"parsed": await resp.json(content_type=None)}) 257 | except (OSError, RuntimeError, ZeroDivisionError): 258 | await self.error() 259 | 260 | return json_return 261 | 262 | async def getWeather(self, weather_url): 263 | """Call endpoint to get local weather from Hive API.""" 264 | json_return = {} 265 | t_url = self.urls["weather"] + weather_url 266 | url = t_url.replace(" ", "%20") 267 | try: 268 | resp = await self.request("get", url) 269 | json_return.update({"original": resp.status}) 270 | json_return.update({"parsed": await resp.json(content_type=None)}) 271 | except (OSError, RuntimeError, ZeroDivisionError, ConnectionError): 272 | await self.error() 273 | 274 | return json_return 275 | 276 | async def setState(self, n_type, n_id, **kwargs): 277 | """Set the state of a Device.""" 278 | json_return = {} 279 | jsc = ( 280 | "{" 281 | + ",".join( 282 | ('"' + str(i) + '": ' '"' + str(t) + '" ' for i, t in kwargs.items()) 283 | ) 284 | + "}" 285 | ) 286 | 287 | url = self.urls["nodes"].format(n_type, n_id) 288 | try: 289 | await self.isFileBeingUsed() 290 | resp = await self.request("post", url, data=jsc) 291 | json_return["original"] = resp.status 292 | json_return["parsed"] = await resp.json(content_type=None) 293 | except (FileInUse, OSError, RuntimeError, ConnectionError) as e: 294 | if e.__class__.__name__ == "FileInUse": 295 | return {"original": "file"} 296 | else: 297 | await self.error() 298 | 299 | return json_return 300 | 301 | async def setAlarm(self, **kwargs): 302 | """Set the state of the alarm.""" 303 | json_return = {} 304 | jsc = ( 305 | "{" 306 | + ",".join( 307 | ('"' + str(i) + '": ' '"' + str(t) + '" ' for i, t in kwargs.items()) 308 | ) 309 | + "}" 310 | ) 311 | 312 | url = f"{self.urls['alarm']}{self.session.config.homeID}" 313 | try: 314 | await self.isFileBeingUsed() 315 | resp = await self.request("post", url, data=jsc) 316 | json_return["original"] = resp.status 317 | json_return["parsed"] = await resp.json(content_type=None) 318 | except (FileInUse, OSError, RuntimeError, ConnectionError) as e: 319 | if e.__class__.__name__ == "FileInUse": 320 | return {"original": "file"} 321 | else: 322 | await self.error() 323 | 324 | return json_return 325 | 326 | async def setAction(self, n_id, data): 327 | """Set the state of a Action.""" 328 | jsc = data 329 | url = self.urls["actions"] + "/" + n_id 330 | try: 331 | await self.isFileBeingUsed() 332 | await self.request("put", url, data=jsc) 333 | except (FileInUse, OSError, RuntimeError, ConnectionError) as e: 334 | if e.__class__.__name__ == "FileInUse": 335 | return {"original": "file"} 336 | else: 337 | await self.error() 338 | 339 | return self.json_return 340 | 341 | async def error(self): 342 | """An error has occurred iteracting with the Hive API.""" 343 | raise web_exceptions.HTTPError 344 | 345 | async def isFileBeingUsed(self): 346 | """Check if running in file mode.""" 347 | if self.session.config.file: 348 | raise FileInUse() 349 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/camera.py: -------------------------------------------------------------------------------- 1 | """Hive Camera Module.""" 2 | # pylint: skip-file 3 | 4 | 5 | class HiveCamera: 6 | """Hive camera. 7 | 8 | Returns: 9 | object: Hive camera 10 | """ 11 | 12 | cameraType = "Camera" 13 | 14 | async def getCameraTemperature(self, device: dict): 15 | """Get the camera state. 16 | 17 | Returns: 18 | boolean: True/False if camera is on. 19 | """ 20 | state = None 21 | 22 | try: 23 | data = self.session.data.devices[device["hiveID"]] 24 | state = data["props"]["temperature"] 25 | except KeyError as e: 26 | await self.session.log.error(e) 27 | 28 | return state 29 | 30 | async def getCameraState(self, device: dict): 31 | """Get the camera state. 32 | 33 | Returns: 34 | boolean: True/False if camera is on. 35 | """ 36 | state = None 37 | 38 | try: 39 | data = self.session.data.devices[device["hiveID"]] 40 | state = True if data["state"]["mode"] == "ARMED" else False 41 | except KeyError as e: 42 | await self.session.log.error(e) 43 | 44 | return state 45 | 46 | async def getCameraImageURL(self, device: dict): 47 | """Get the camera image url. 48 | 49 | Returns: 50 | str: image url. 51 | """ 52 | state = None 53 | 54 | try: 55 | state = self.session.data.camera[device["hiveID"]]["cameraImage"][ 56 | "thumbnailUrls" 57 | ][0] 58 | except KeyError as e: 59 | await self.session.log.error(e) 60 | 61 | return state 62 | 63 | async def getCameraRecodringURL(self, device: dict): 64 | """Get the camera recording url. 65 | 66 | Returns: 67 | str: image url. 68 | """ 69 | state = None 70 | 71 | try: 72 | state = self.session.data.camera[device["hiveID"]]["cameraRecording"] 73 | except KeyError as e: 74 | await self.session.log.error(e) 75 | 76 | return state 77 | 78 | async def setCameraOn(self, device: dict, mode: str): 79 | """Set the camera state to on. 80 | 81 | Args: 82 | device (dict): Camera device. 83 | 84 | Returns: 85 | boolean: True/False if successful. 86 | """ 87 | final = False 88 | 89 | if ( 90 | device["hiveID"] in self.session.data.devices 91 | and device["deviceData"]["online"] 92 | ): 93 | await self.session.hiveRefreshTokens() 94 | resp = await self.session.api.setState(mode=mode) 95 | if resp["original"] == 200: 96 | final = True 97 | await self.session.getCamera() 98 | 99 | return final 100 | 101 | async def setCameraOff(self, device: dict, mode: str): 102 | """Set the camera state to on. 103 | 104 | Args: 105 | device (dict): Camera device. 106 | 107 | Returns: 108 | boolean: True/False if successful. 109 | """ 110 | final = False 111 | 112 | if ( 113 | device["hiveID"] in self.session.data.devices 114 | and device["deviceData"]["online"] 115 | ): 116 | await self.session.hiveRefreshTokens() 117 | resp = await self.session.api.setState(mode=mode) 118 | if resp["original"] == 200: 119 | final = True 120 | await self.session.getCamera() 121 | 122 | return final 123 | 124 | 125 | class Camera(HiveCamera): 126 | """Home assistant camera. 127 | 128 | Args: 129 | HiveCamera (object): Class object. 130 | """ 131 | 132 | def __init__(self, session: object = None): 133 | """Initialise camera. 134 | 135 | Args: 136 | session (object, optional): Used to interact with the hive account. Defaults to None. 137 | """ 138 | self.session = session 139 | 140 | async def getCamera(self, device: dict): 141 | """Get camera data. 142 | 143 | Args: 144 | device (dict): Device to update. 145 | 146 | Returns: 147 | dict: Updated device. 148 | """ 149 | device["deviceData"].update( 150 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 151 | ) 152 | dev_data = {} 153 | 154 | if device["deviceData"]["online"]: 155 | self.session.helper.deviceRecovered(device["device_id"]) 156 | data = self.session.data.devices[device["device_id"]] 157 | dev_data = { 158 | "hiveID": device["hiveID"], 159 | "hiveName": device["hiveName"], 160 | "hiveType": device["hiveType"], 161 | "haName": device["haName"], 162 | "haType": device["haType"], 163 | "device_id": device["device_id"], 164 | "device_name": device["device_name"], 165 | "status": { 166 | "temperature": await self.getCameraTemperature(device), 167 | "state": await self.getCameraState(device), 168 | "imageURL": await self.getCameraImageURL(device), 169 | "recordingURL": await self.getCameraRecodringURL(device), 170 | }, 171 | "deviceData": data.get("props", None), 172 | "parentDevice": data.get("parent", None), 173 | "custom": device.get("custom", None), 174 | "attributes": await self.session.attr.stateAttributes( 175 | device["device_id"], device["hiveType"] 176 | ), 177 | } 178 | 179 | self.session.devices.update({device["hiveID"]: dev_data}) 180 | return self.session.devices[device["hiveID"]] 181 | else: 182 | await self.session.log.errorCheck( 183 | device["device_id"], "ERROR", device["deviceData"]["online"] 184 | ) 185 | return device 186 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/data/alarm.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "home", 3 | "securitySystemState": "ARMED", 4 | "armingGracePeriod": 60, 5 | "alarmingGracePeriod": 60, 6 | "devices": [ 7 | "keypad-0000-0000-000000000001", 8 | "contact-sensor-0000-0000-000000000001", 9 | "siren-0000-0000-000000000001", 10 | "contact-sensor-0000-0000-000000000002" 11 | ], 12 | "triggers": { 13 | "home": [], 14 | "away": [ 15 | "contact-sensor-0000-0000-000000000001", 16 | "contact-sensor-0000-0000-000000000002" 17 | ], 18 | "asleep": [ 19 | "contact-sensor-0000-0000-000000000002", 20 | "contact-sensor-0000-0000-000000000001" 21 | ] 22 | }, 23 | "actions": { 24 | "away": [ 25 | "keypad-0000-0000-000000000001" 26 | ], 27 | "asleep": [ 28 | "keypad-0000-0000-000000000001" 29 | ], 30 | "sos": [ 31 | "keypad-0000-0000-000000000001" 32 | ] 33 | }, 34 | "monitoringCameras": { 35 | "home": [], 36 | "away": [], 37 | "asleep": [] 38 | }, 39 | "alarmingGraceChirp": { 40 | "away": "ON_GRACE_START", 41 | "asleep": "ON_GRACE_START" 42 | }, 43 | "numberOfTriggersToAlarm": { 44 | "away": 1, 45 | "asleep": 1 46 | }, 47 | "modeValid": { 48 | "home": true, 49 | "away": true, 50 | "asleep": true 51 | }, 52 | "pinSchedules": {} 53 | } -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/data/camera.json: -------------------------------------------------------------------------------- 1 | { 2 | "cameraImage": { 3 | "parsed": { 4 | "events": [ 5 | { 6 | "thumbnailUrls": [ 7 | "https://test.com/image" 8 | ], 9 | "hasRecording": true 10 | } 11 | ] 12 | } 13 | }, 14 | "camaeraRecording": { 15 | "parsed": "https://test.com/video" 16 | } 17 | } -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/device_attributes.py: -------------------------------------------------------------------------------- 1 | """Hive Device Attribute Module.""" 2 | # pylint: skip-file 3 | from .helper.const import HIVETOHA 4 | from .helper.logger import Logger 5 | 6 | 7 | class HiveAttributes: 8 | """Device Attributes Code.""" 9 | 10 | def __init__(self, session: object = None): 11 | """Initialise attributes. 12 | 13 | Args: 14 | session (object, optional): Session to interact with hive account. Defaults to None. 15 | """ 16 | self.session = session 17 | self.session.log = Logger() 18 | self.type = "Attribute" 19 | 20 | async def stateAttributes(self, n_id: str, _type: str): 21 | """Get HA State Attributes. 22 | 23 | Args: 24 | n_id (str): The id of the device. 25 | _type (str): The device type. 26 | 27 | Returns: 28 | dict: Set of attributes. 29 | """ 30 | attr = {} 31 | 32 | if n_id in self.session.data.products or n_id in self.session.data.devices: 33 | attr.update({"available": (await self.onlineOffline(n_id))}) 34 | if n_id in self.session.config.battery: 35 | battery = await self.getBattery(n_id) 36 | if battery is not None: 37 | attr.update({"battery": str(battery) + "%"}) 38 | if n_id in self.session.config.mode: 39 | attr.update({"mode": (await self.getMode(n_id))}) 40 | return attr 41 | 42 | async def onlineOffline(self, n_id: str): 43 | """Check if device is online. 44 | 45 | Args: 46 | n_id (str): The id of the device. 47 | 48 | Returns: 49 | boolean: True/False if device online. 50 | """ 51 | state = None 52 | 53 | try: 54 | data = self.session.data.devices[n_id] 55 | state = data["props"]["online"] 56 | except KeyError as e: 57 | await self.session.log.error(e) 58 | 59 | return state 60 | 61 | async def getMode(self, n_id: str): 62 | """Get sensor mode. 63 | 64 | Args: 65 | n_id (str): The id of the device 66 | 67 | Returns: 68 | str: The mode of the device. 69 | """ 70 | state = None 71 | final = None 72 | 73 | try: 74 | data = self.session.data.products[n_id] 75 | state = data["state"]["mode"] 76 | final = HIVETOHA[self.type].get(state, state) 77 | except KeyError as e: 78 | await self.session.log.error(e) 79 | 80 | return final 81 | 82 | async def getBattery(self, n_id: str): 83 | """Get device battery level. 84 | 85 | Args: 86 | n_id (str): The id of the device. 87 | 88 | Returns: 89 | str: Battery level of device. 90 | """ 91 | state = None 92 | final = None 93 | 94 | try: 95 | data = self.session.data.devices[n_id] 96 | state = data["props"]["battery"] 97 | final = state 98 | await self.session.log.errorCheck(n_id, self.type, state) 99 | except KeyError as e: 100 | await self.session.log.error(e) 101 | 102 | return final 103 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/heating.py: -------------------------------------------------------------------------------- 1 | """Hive Heating Module.""" 2 | # pylint: skip-file 3 | from .helper.const import HIVETOHA 4 | 5 | 6 | class HiveHeating: 7 | """Hive Heating Code. 8 | 9 | Returns: 10 | object: heating 11 | """ 12 | 13 | heatingType = "Heating" 14 | 15 | async def getMinTemperature(self, device: dict): 16 | """Get heating minimum target temperature. 17 | 18 | Args: 19 | device (dict): Device to get min temp for. 20 | 21 | Returns: 22 | int: Minimum temperature 23 | """ 24 | if device["hiveType"] == "nathermostat": 25 | return self.session.data.products[device["hiveID"]]["props"]["minHeat"] 26 | return 5 27 | 28 | async def getMaxTemperature(self, device: dict): 29 | """Get heating maximum target temperature. 30 | 31 | Args: 32 | device (dict): Device to get max temp for. 33 | 34 | Returns: 35 | int: Maximum temperature 36 | """ 37 | if device["hiveType"] == "nathermostat": 38 | return self.session.data.products[device["hiveID"]]["props"]["maxHeat"] 39 | return 32 40 | 41 | async def getCurrentTemperature(self, device: dict): 42 | """Get heating current temperature. 43 | 44 | Args: 45 | device (dict): Device to get current temperature for. 46 | 47 | Returns: 48 | float: current temperature 49 | """ 50 | from datetime import datetime 51 | 52 | f_state = None 53 | state = None 54 | final = None 55 | 56 | try: 57 | data = self.session.data.products[device["hiveID"]] 58 | state = data["props"]["temperature"] 59 | 60 | if device["hiveID"] in self.session.data.minMax: 61 | if self.session.data.minMax[device["hiveID"]]["TodayDate"] == str( 62 | datetime.date(datetime.now()) 63 | ): 64 | if state < self.session.data.minMax[device["hiveID"]]["TodayMin"]: 65 | self.session.data.minMax[device["hiveID"]]["TodayMin"] = state 66 | 67 | if state > self.session.data.minMax[device["hiveID"]]["TodayMax"]: 68 | self.session.data.minMax[device["hiveID"]]["TodayMax"] = state 69 | else: 70 | data = { 71 | "TodayMin": state, 72 | "TodayMax": state, 73 | "TodayDate": str(datetime.date(datetime.now())), 74 | } 75 | self.session.data.minMax[device["hiveID"]].update(data) 76 | 77 | if state < self.session.data.minMax[device["hiveID"]]["RestartMin"]: 78 | self.session.data.minMax[device["hiveID"]]["RestartMin"] = state 79 | 80 | if state > self.session.data.minMax[device["hiveID"]]["RestartMax"]: 81 | self.session.data.minMax[device["hiveID"]]["RestartMax"] = state 82 | else: 83 | data = { 84 | "TodayMin": state, 85 | "TodayMax": state, 86 | "TodayDate": str(datetime.date(datetime.now())), 87 | "RestartMin": state, 88 | "RestartMax": state, 89 | } 90 | self.session.data.minMax[device["hiveID"]] = data 91 | 92 | f_state = round(float(state), 1) 93 | final = f_state 94 | except KeyError as e: 95 | await self.session.log.error(e) 96 | 97 | return final 98 | 99 | async def getTargetTemperature(self, device: dict): 100 | """Get heating target temperature. 101 | 102 | Args: 103 | device (dict): Device to get target temperature for. 104 | 105 | Returns: 106 | str: Target temperature. 107 | """ 108 | state = None 109 | 110 | try: 111 | data = self.session.data.products[device["hiveID"]] 112 | state = float(data["state"].get("target", None)) 113 | state = float(data["state"].get("heat", state)) 114 | except (KeyError, TypeError) as e: 115 | await self.session.log.error(e) 116 | 117 | return state 118 | 119 | async def getMode(self, device: dict): 120 | """Get heating current mode. 121 | 122 | Args: 123 | device (dict): Device to get current mode for. 124 | 125 | Returns: 126 | str: Current Mode 127 | """ 128 | state = None 129 | final = None 130 | 131 | try: 132 | data = self.session.data.products[device["hiveID"]] 133 | state = data["state"]["mode"] 134 | if state == "BOOST": 135 | state = data["props"]["previous"]["mode"] 136 | final = HIVETOHA[self.heatingType].get(state, state) 137 | except KeyError as e: 138 | await self.session.log.error(e) 139 | 140 | return final 141 | 142 | async def getState(self, device: dict): 143 | """Get heating current state. 144 | 145 | Args: 146 | device (dict): Device to get state for. 147 | 148 | Returns: 149 | str: Current state. 150 | """ 151 | state = None 152 | final = None 153 | 154 | try: 155 | current_temp = await self.getCurrentTemperature(device) 156 | target_temp = await self.getTargetTemperature(device) 157 | if current_temp < target_temp: 158 | state = "ON" 159 | else: 160 | state = "OFF" 161 | final = HIVETOHA[self.heatingType].get(state, state) 162 | except KeyError as e: 163 | await self.session.log.error(e) 164 | 165 | return final 166 | 167 | async def getCurrentOperation(self, device: dict): 168 | """Get heating current operation. 169 | 170 | Args: 171 | device (dict): Device to get current operation for. 172 | 173 | Returns: 174 | str: Current operation. 175 | """ 176 | state = None 177 | 178 | try: 179 | data = self.session.data.products[device["hiveID"]] 180 | state = data["props"]["working"] 181 | except KeyError as e: 182 | await self.session.log.error(e) 183 | 184 | return state 185 | 186 | async def getBoostStatus(self, device: dict): 187 | """Get heating boost current status. 188 | 189 | Args: 190 | device (dict): Device to get boost status for. 191 | 192 | Returns: 193 | str: Boost status. 194 | """ 195 | state = None 196 | 197 | try: 198 | data = self.session.data.products[device["hiveID"]] 199 | state = HIVETOHA["Boost"].get(data["state"].get("boost", False), "ON") 200 | except KeyError as e: 201 | await self.session.log.error(e) 202 | 203 | return state 204 | 205 | async def getBoostTime(self, device: dict): 206 | """Get heating boost time remaining. 207 | 208 | Args: 209 | device (dict): device to get boost time for. 210 | 211 | Returns: 212 | str: Boost time. 213 | """ 214 | if await self.getBoostStatus(device) == "ON": 215 | state = None 216 | 217 | try: 218 | data = self.session.data.products[device["hiveID"]] 219 | state = data["state"]["boost"] 220 | except KeyError as e: 221 | await self.session.log.error(e) 222 | 223 | return state 224 | return None 225 | 226 | async def getHeatOnDemand(self, device): 227 | """Get heat on demand status. 228 | 229 | Args: 230 | device ([dictionary]): [Get Heat on Demand status for Thermostat device.] 231 | 232 | Returns: 233 | str: [Return True or False for the Heat on Demand status.] 234 | """ 235 | state = None 236 | 237 | try: 238 | data = self.session.data.products[device["hiveID"]] 239 | state = data["props"]["autoBoost"]["active"] 240 | except KeyError as e: 241 | await self.session.log.error(e) 242 | 243 | return state 244 | 245 | @staticmethod 246 | async def getOperationModes(): 247 | """Get heating list of possible modes. 248 | 249 | Returns: 250 | list: Operation modes. 251 | """ 252 | return ["SCHEDULE", "MANUAL", "OFF"] 253 | 254 | async def setTargetTemperature(self, device: dict, new_temp: str): 255 | """Set heating target temperature. 256 | 257 | Args: 258 | device (dict): Device to set target temperature for. 259 | new_temp (str): New temperature. 260 | 261 | Returns: 262 | boolean: True/False if successful 263 | """ 264 | await self.session.hiveRefreshTokens() 265 | final = False 266 | 267 | if ( 268 | device["hiveID"] in self.session.data.products 269 | and device["deviceData"]["online"] 270 | ): 271 | await self.session.hiveRefreshTokens() 272 | data = self.session.data.products[device["hiveID"]] 273 | resp = await self.session.api.setState( 274 | data["type"], device["hiveID"], target=new_temp 275 | ) 276 | 277 | if resp["original"] == 200: 278 | await self.session.getDevices(device["hiveID"]) 279 | final = True 280 | 281 | return final 282 | 283 | async def setMode(self, device: dict, new_mode: str): 284 | """Set heating mode. 285 | 286 | Args: 287 | device (dict): Device to set mode for. 288 | new_mode (str): New mode to be set. 289 | 290 | Returns: 291 | boolean: True/False if successful 292 | """ 293 | await self.session.hiveRefreshTokens() 294 | final = False 295 | 296 | if ( 297 | device["hiveID"] in self.session.data.products 298 | and device["deviceData"]["online"] 299 | ): 300 | data = self.session.data.products[device["hiveID"]] 301 | resp = await self.session.api.setState( 302 | data["type"], device["hiveID"], mode=new_mode 303 | ) 304 | 305 | if resp["original"] == 200: 306 | await self.session.getDevices(device["hiveID"]) 307 | final = True 308 | 309 | return final 310 | 311 | async def setBoostOn(self, device: dict, mins: str, temp: float): 312 | """Turn heating boost on. 313 | 314 | Args: 315 | device (dict): Device to boost. 316 | mins (str): Number of minutes to boost for. 317 | temp (float): Temperature to boost to. 318 | 319 | Returns: 320 | boolean: True/False if successful 321 | """ 322 | if int(mins) > 0 and int(temp) >= await self.getMinTemperature(device): 323 | if int(temp) <= await self.getMaxTemperature(device): 324 | await self.session.hiveRefreshTokens() 325 | final = False 326 | 327 | if ( 328 | device["hiveID"] in self.session.data.products 329 | and device["deviceData"]["online"] 330 | ): 331 | data = self.session.data.products[device["hiveID"]] 332 | resp = await self.session.api.setState( 333 | data["type"], 334 | device["hiveID"], 335 | mode="BOOST", 336 | boost=mins, 337 | target=temp, 338 | ) 339 | 340 | if resp["original"] == 200: 341 | await self.session.getDevices(device["hiveID"]) 342 | final = True 343 | 344 | return final 345 | return None 346 | 347 | async def setBoostOff(self, device: dict): 348 | """Turn heating boost off. 349 | 350 | Args: 351 | device (dict): Device to update boost for. 352 | 353 | Returns: 354 | boolean: True/False if successful 355 | """ 356 | final = False 357 | 358 | if ( 359 | device["hiveID"] in self.session.data.products 360 | and device["deviceData"]["online"] 361 | ): 362 | await self.session.hiveRefreshTokens() 363 | data = self.session.data.products[device["hiveID"]] 364 | await self.session.getDevices(device["hiveID"]) 365 | if await self.getBoostStatus(device) == "ON": 366 | prev_mode = data["props"]["previous"]["mode"] 367 | if prev_mode == "MANUAL" or prev_mode == "OFF": 368 | pre_temp = data["props"]["previous"].get("target", 7) 369 | resp = await self.session.api.setState( 370 | data["type"], 371 | device["hiveID"], 372 | mode=prev_mode, 373 | target=pre_temp, 374 | ) 375 | else: 376 | resp = await self.session.api.setState( 377 | data["type"], device["hiveID"], mode=prev_mode 378 | ) 379 | if resp["original"] == 200: 380 | await self.session.getDevices(device["hiveID"]) 381 | final = True 382 | 383 | return final 384 | 385 | async def setHeatOnDemand(self, device: dict, state: str): 386 | """Enable or disable Heat on Demand for a Thermostat. 387 | 388 | Args: 389 | device ([dictionary]): [This is the Thermostat device you want to update.] 390 | state ([str]): [This is the state you want to set. (Either "ENABLED" or "DISABLED")] 391 | 392 | Returns: 393 | [boolean]: [Return True or False if the Heat on Demand was set successfully.] 394 | """ 395 | final = False 396 | 397 | if ( 398 | device["hiveID"] in self.session.data.products 399 | and device["deviceData"]["online"] 400 | ): 401 | data = self.session.data.products[device["hiveID"]] 402 | await self.session.hiveRefreshTokens() 403 | resp = await self.session.api.setState( 404 | data["type"], device["hiveID"], autoBoost=state 405 | ) 406 | 407 | if resp["original"] == 200: 408 | await self.session.getDevices(device["hiveID"]) 409 | final = True 410 | 411 | return final 412 | 413 | 414 | class Climate(HiveHeating): 415 | """Climate class for Home Assistant. 416 | 417 | Args: 418 | Heating (object): Heating class 419 | """ 420 | 421 | def __init__(self, session: object = None): 422 | """Initialise heating. 423 | 424 | Args: 425 | session (object, optional): Used to interact with hive account. Defaults to None. 426 | """ 427 | self.session = session 428 | 429 | async def getClimate(self, device: dict): 430 | """Get heating data. 431 | 432 | Args: 433 | device (dict): Device to update. 434 | 435 | Returns: 436 | dict: Updated device. 437 | """ 438 | device["deviceData"].update( 439 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 440 | ) 441 | 442 | if device["deviceData"]["online"]: 443 | dev_data = {} 444 | self.session.helper.deviceRecovered(device["device_id"]) 445 | data = self.session.data.devices[device["device_id"]] 446 | dev_data = { 447 | "hiveID": device["hiveID"], 448 | "hiveName": device["hiveName"], 449 | "hiveType": device["hiveType"], 450 | "haName": device["haName"], 451 | "haType": device["haType"], 452 | "device_id": device["device_id"], 453 | "device_name": device["device_name"], 454 | "temperatureunit": device["temperatureunit"], 455 | "min_temp": await self.getMinTemperature(device), 456 | "max_temp": await self.getMaxTemperature(device), 457 | "status": { 458 | "current_temperature": await self.getCurrentTemperature(device), 459 | "target_temperature": await self.getTargetTemperature(device), 460 | "action": await self.getCurrentOperation(device), 461 | "mode": await self.getMode(device), 462 | "boost": await self.getBoostStatus(device), 463 | }, 464 | "deviceData": data.get("props", None), 465 | "parentDevice": data.get("parent", None), 466 | "custom": device.get("custom", None), 467 | "attributes": await self.session.attr.stateAttributes( 468 | device["device_id"], device["hiveType"] 469 | ), 470 | } 471 | self.session.devices.update({device["hiveID"]: dev_data}) 472 | return self.session.devices[device["hiveID"]] 473 | else: 474 | await self.session.log.errorCheck( 475 | device["device_id"], "ERROR", device["deviceData"]["online"] 476 | ) 477 | return device 478 | 479 | async def getScheduleNowNextLater(self, device: dict): 480 | """Hive get heating schedule now, next and later. 481 | 482 | Args: 483 | device (dict): Device to get schedule for. 484 | 485 | Returns: 486 | dict: Schedule now, next and later 487 | """ 488 | online = await self.session.attr.onlineOffline(device["device_id"]) 489 | current_mode = await self.getMode(device) 490 | state = None 491 | 492 | try: 493 | if online and current_mode == "SCHEDULE": 494 | data = self.session.data.products[device["hiveID"]] 495 | state = self.session.helper.getScheduleNNL(data["state"]["schedule"]) 496 | except KeyError as e: 497 | await self.session.log.error(e) 498 | 499 | return state 500 | 501 | async def minmaxTemperature(self, device: dict): 502 | """Min/Max Temp. 503 | 504 | Args: 505 | device (dict): device to get min/max temperature for. 506 | 507 | Returns: 508 | dict: Shows min/max temp for the day. 509 | """ 510 | state = None 511 | final = None 512 | 513 | try: 514 | state = self.session.data.minMax[device["hiveID"]] 515 | final = state 516 | except KeyError as e: 517 | await self.session.log.error(e) 518 | 519 | return final 520 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/__init__.py: -------------------------------------------------------------------------------- 1 | """__init__.py file.""" 2 | # pylint: skip-file 3 | from .hive_helper import HiveHelper # noqa: F401 4 | from .logger import Logger # noqa: F401 5 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/const.py: -------------------------------------------------------------------------------- 1 | """Constants for Pyhiveapi.""" 2 | # pylint: skip-file 3 | SYNC_PACKAGE_NAME = "pyhiveapi" 4 | SYNC_PACKAGE_DIR = "/pyhiveapi/" 5 | ASYNC_PACKAGE_NAME = "apyhiveapi" 6 | ASYNC_PACKAGE_DIR = "/apyhiveapi/" 7 | SMS_REQUIRED = "SMS_MFA" 8 | 9 | 10 | # HTTP return codes. 11 | HTTP_OK = 200 12 | HTTP_CREATED = 201 13 | HTTP_ACCEPTED = 202 14 | HTTP_MOVED_PERMANENTLY = 301 15 | HTTP_BAD_REQUEST = 400 16 | HTTP_UNAUTHORIZED = 401 17 | HTTP_FORBIDDEN = 403 18 | HTTP_NOT_FOUND = 404 19 | HTTP_METHOD_NOT_ALLOWED = 405 20 | HTTP_UNPROCESSABLE_ENTITY = 422 21 | HTTP_TOO_MANY_REQUESTS = 429 22 | HTTP_INTERNAL_SERVER_ERROR = 500 23 | HTTP_BAD_GATEWAY = 502 24 | HTTP_SERVICE_UNAVAILABLE = 503 25 | 26 | 27 | HIVETOHA = { 28 | "Alarm": {"home": "armed_home", "away": "armed_away", "asleep": "armed_night"}, 29 | "Attribute": {True: "Online", False: "Offline"}, 30 | "Boost": {None: "OFF", False: "OFF"}, 31 | "Heating": {False: "OFF", "ENABLED": True, "DISABLED": False}, 32 | "Hotwater": {"MANUAL": "ON", None: "OFF", False: "OFF"}, 33 | "Hub": { 34 | "Status": {True: 1, False: 0}, 35 | "Smoke": {True: 1, False: 0}, 36 | "Dog": {True: 1, False: 0}, 37 | "Glass": {True: 1, False: 0}, 38 | }, 39 | "Light": {"ON": True, "OFF": False}, 40 | "Sensor": { 41 | "OPEN": True, 42 | "CLOSED": False, 43 | True: "Online", 44 | False: "Offline", 45 | }, 46 | "Switch": {"ON": True, "OFF": False}, 47 | } 48 | 49 | HIVE_TYPES = { 50 | "Hub": ["hub", "sense"], 51 | "Thermo": ["thermostatui", "trv"], 52 | "Heating": ["heating", "trvcontrol"], 53 | "Hotwater": ["hotwater"], 54 | "Light": ["warmwhitelight", "tuneablelight", "colourtuneablelight"], 55 | "Sensor": ["motionsensor", "contactsensor"], 56 | "Switch": ["activeplug"], 57 | } 58 | sensor_commands = { 59 | "SMOKE_CO": "self.session.hub.getSmokeStatus(device)", 60 | "DOG_BARK": "self.session.hub.getDogBarkStatus(device)", 61 | "GLASS_BREAK": "self.session.hub.getGlassBreakStatus(device)", 62 | "Camera_Temp": "self.session.camera.getCameraTemperature(device)", 63 | "Current_Temperature": "self.session.heating.getCurrentTemperature(device)", 64 | "Heating_Current_Temperature": "self.session.heating.getCurrentTemperature(device)", 65 | "Heating_Target_Temperature": "self.session.heating.getTargetTemperature(device)", 66 | "Heating_State": "self.session.heating.getState(device)", 67 | "Heating_Mode": "self.session.heating.getMode(device)", 68 | "Heating_Boost": "self.session.heating.getBoostStatus(device)", 69 | "Hotwater_State": "self.session.hotwater.getState(device)", 70 | "Hotwater_Mode": "self.session.hotwater.getMode(device)", 71 | "Hotwater_Boost": "self.session.hotwater.getBoost(device)", 72 | "Battery": 'self.session.attr.getBattery(device["device_id"])', 73 | "Mode": 'self.session.attr.getMode(device["hiveID"])', 74 | "Availability": "self.online(device)", 75 | "Connectivity": "self.online(device)", 76 | "Power": "self.session.switch.getPowerUsage(device)", 77 | } 78 | 79 | PRODUCTS = { 80 | "sense": [ 81 | 'addList("binary_sensor", p, haName="Glass Detection", hiveType="GLASS_BREAK")', 82 | 'addList("binary_sensor", p, haName="Smoke Detection", hiveType="SMOKE_CO")', 83 | 'addList("binary_sensor", p, haName="Dog Bark Detection", hiveType="DOG_BARK")', 84 | ], 85 | "heating": [ 86 | 'addList("climate", p, temperatureunit=self.data["user"]["temperatureUnit"])', 87 | 'addList("switch", p, haName=" Heat on Demand", hiveType="Heating_Heat_On_Demand", category="config")', 88 | 'addList("sensor", p, haName=" Current Temperature", hiveType="Heating_Current_Temperature", category="diagnostic")', 89 | 'addList("sensor", p, haName=" Target Temperature", hiveType="Heating_Target_Temperature", category="diagnostic")', 90 | 'addList("sensor", p, haName=" State", hiveType="Heating_State", category="diagnostic")', 91 | 'addList("sensor", p, haName=" Mode", hiveType="Heating_Mode", category="diagnostic")', 92 | 'addList("sensor", p, haName=" Boost", hiveType="Heating_Boost", category="diagnostic")', 93 | ], 94 | "trvcontrol": [ 95 | 'addList("climate", p, temperatureunit=self.data["user"]["temperatureUnit"])', 96 | 'addList("sensor", p, haName=" Current Temperature", hiveType="Heating_Current_Temperature", category="diagnostic")', 97 | 'addList("sensor", p, haName=" Target Temperature", hiveType="Heating_Target_Temperature", category="diagnostic")', 98 | 'addList("sensor", p, haName=" State", hiveType="Heating_State", category="diagnostic")', 99 | 'addList("sensor", p, haName=" Mode", hiveType="Heating_Mode", category="diagnostic")', 100 | 'addList("sensor", p, haName=" Boost", hiveType="Heating_Boost", category="diagnostic")', 101 | ], 102 | "hotwater": [ 103 | 'addList("water_heater", p,)', 104 | 'addList("sensor", p, haName="Hotwater State", hiveType="Hotwater_State", category="diagnostic")', 105 | 'addList("sensor", p, haName="Hotwater Mode", hiveType="Hotwater_Mode", category="diagnostic")', 106 | 'addList("sensor", p, haName="Hotwater Boost", hiveType="Hotwater_Boost", category="diagnostic")', 107 | ], 108 | "activeplug": [ 109 | 'addList("switch", p)', 110 | 'addList("sensor", p, haName=" Mode", hiveType="Mode", category="diagnostic")', 111 | 'addList("sensor", p, haName=" Availability", hiveType="Availability", category="diagnostic")', 112 | 'addList("sensor", p, haName=" Power", hiveType="Power", category="diagnostic")', 113 | ], 114 | "warmwhitelight": [ 115 | 'addList("light", p)', 116 | 'addList("sensor", p, haName=" Mode", hiveType="Mode", category="diagnostic")', 117 | 'addList("sensor", p, haName=" Availability", hiveType="Availability", category="diagnostic")', 118 | ], 119 | "tuneablelight": [ 120 | 'addList("light", p)', 121 | 'addList("sensor", p, haName=" Mode", hiveType="Mode", category="diagnostic")', 122 | 'addList("sensor", p, haName=" Availability", hiveType="Availability", category="diagnostic")', 123 | ], 124 | "colourtuneablelight": [ 125 | 'addList("light", p)', 126 | 'addList("sensor", p, haName=" Mode", hiveType="Mode", category="diagnostic")', 127 | 'addList("sensor", p, haName=" Availability", hiveType="Availability", category="diagnostic")', 128 | ], 129 | # "hivecamera": [ 130 | # 'addList("camera", p)', 131 | # 'addList("sensor", p, haName=" Mode", hiveType="Mode", category="diagnostic")', 132 | # 'addList("sensor", p, haName=" Availability", hiveType="Availability", category="diagnostic")', 133 | # 'addList("sensor", p, haName=" Temperature", hiveType="Camera_Temp", category="diagnostic")', 134 | # ], 135 | "motionsensor": [ 136 | 'addList("binary_sensor", p)', 137 | 'addList("sensor", p, haName=" Current Temperature", hiveType="Current_Temperature", category="diagnostic")', 138 | ], 139 | "contactsensor": ['addList("binary_sensor", p)'], 140 | } 141 | 142 | DEVICES = { 143 | "contactsensor": [ 144 | 'addList("sensor", d, haName=" Battery Level", hiveType="Battery", category="diagnostic")', 145 | 'addList("sensor", d, haName=" Availability", hiveType="Availability", category="diagnostic")', 146 | ], 147 | "hub": [ 148 | 'addList("binary_sensor", d, haName="Hive Hub Status", hiveType="Connectivity", category="diagnostic")', 149 | ], 150 | "motionsensor": [ 151 | 'addList("sensor", d, haName=" Battery Level", hiveType="Battery", category="diagnostic")', 152 | 'addList("sensor", d, haName=" Availability", hiveType="Availability", category="diagnostic")', 153 | ], 154 | "sense": [ 155 | 'addList("binary_sensor", d, haName="Hive Hub Status", hiveType="Connectivity")', 156 | ], 157 | "siren": ['addList("alarm_control_panel", d)'], 158 | "thermostatui": [ 159 | 'addList("sensor", d, haName=" Battery Level", hiveType="Battery", category="diagnostic")', 160 | 'addList("sensor", d, haName=" Availability", hiveType="Availability", category="diagnostic")', 161 | ], 162 | "trv": [ 163 | 'addList("sensor", d, haName=" Battery Level", hiveType="Battery", category="diagnostic")', 164 | 'addList("sensor", d, haName=" Availability", hiveType="Availability", category="diagnostic")', 165 | ], 166 | } 167 | 168 | ACTIONS = ( 169 | 'addList("switch", a, hiveName=a["name"], haName=a["name"], hiveType="action")' 170 | ) 171 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/debugger.py: -------------------------------------------------------------------------------- 1 | """Debugger file.""" 2 | # pylint: skip-file 3 | import logging 4 | 5 | 6 | class DebugContext: 7 | """Debug context to trace any function calls inside the context.""" 8 | 9 | def __init__(self, name, enabled): 10 | """Initialise debugger.""" 11 | self.name = name 12 | self.enabled = enabled 13 | self.logging = logging.getLogger(__name__) 14 | self.debugOutFolder = "" 15 | self.debugOutFile = "" 16 | self.debugEnabled = False 17 | self.debugList = [] 18 | 19 | def __enter__(self): 20 | """Set trace calls on entering debugger.""" 21 | print("Entering Debug Decorated func") 22 | # Set the trace function to the trace_calls function 23 | # So all events are now traced 24 | self.traceCalls 25 | 26 | def traceCalls(self, frame, event, arg): 27 | """Trace calls be made.""" 28 | # We want to only trace our call to the decorated function 29 | if event != "call": 30 | return 31 | elif frame.f_code.co_name != self.name: 32 | return 33 | # return the trace function to use when you go into that 34 | # function call 35 | return self.traceLines 36 | 37 | def traceLines(self, frame, event, arg): 38 | """Print out lines for function.""" 39 | # If you want to print local variables each line 40 | # keep the check for the event 'line' 41 | # If you want to print local variables only on return 42 | # check only for the 'return' event 43 | if event not in ["line", "return"]: 44 | return 45 | co = frame.f_code 46 | func_name = co.co_name 47 | line_no = frame.f_lineno 48 | local_vars = frame.f_locals 49 | text = f" {func_name} {event} {line_no} locals: {local_vars}" 50 | self.logging.debug(text) 51 | 52 | 53 | def debug(enabled=False): 54 | """Debug decorator to call the function within the debug context.""" 55 | 56 | def decorated_func(func): 57 | def wrapper(*args, **kwargs): 58 | with DebugContext(func.__name__, enabled): 59 | return_value = func(*args, **kwargs) 60 | return return_value 61 | 62 | return wrapper 63 | 64 | return decorated_func 65 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/hive_exceptions.py: -------------------------------------------------------------------------------- 1 | """Hive exception class.""" 2 | # pylint: skip-file 3 | 4 | 5 | class FileInUse(Exception): 6 | """File in use exception. 7 | 8 | Args: 9 | Exception (object): Exception object to invoke 10 | """ 11 | 12 | 13 | class NoApiToken(Exception): 14 | """No API token exception. 15 | 16 | Args: 17 | Exception (object): Exception object to invoke 18 | """ 19 | 20 | 21 | class HiveApiError(Exception): 22 | """Api error. 23 | 24 | Args: 25 | Exception (object): Exception object to invoke 26 | """ 27 | 28 | 29 | class HiveReauthRequired(Exception): 30 | """Re-Authentication is required. 31 | 32 | Args: 33 | Exception (object): Exception object to invoke 34 | """ 35 | 36 | 37 | class HiveUnknownConfiguration(Exception): 38 | """Unknown Hive Configuration. 39 | 40 | Args: 41 | Exception (object): Exception object to invoke 42 | """ 43 | 44 | 45 | class HiveInvalidUsername(Exception): 46 | """Raise invalid Username. 47 | 48 | Args: 49 | Exception (object): Exception object to invoke 50 | """ 51 | 52 | 53 | class HiveInvalidPassword(Exception): 54 | """Raise invalid password. 55 | 56 | Args: 57 | Exception (object): Exception object to invoke 58 | """ 59 | 60 | 61 | class HiveInvalid2FACode(Exception): 62 | """Raise invalid 2FA code. 63 | 64 | Args: 65 | Exception (object): Exception object to invoke 66 | """ 67 | 68 | 69 | class HiveInvalidDeviceAuthentication(Exception): 70 | """Raise invalid device authentication. 71 | 72 | Args: 73 | Exception (object): Exception object to invoke 74 | """ 75 | 76 | 77 | class HiveFailedToRefreshTokens(Exception): 78 | """Raise invalid refresh tokens. 79 | 80 | Args: 81 | Exception (object): Exception object to invoke 82 | """ 83 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/hive_helper.py: -------------------------------------------------------------------------------- 1 | """Helper class for pyhiveapi.""" 2 | # pylint: skip-file 3 | import datetime 4 | import operator 5 | 6 | from .const import HIVE_TYPES 7 | 8 | 9 | class HiveHelper: 10 | """Hive helper class.""" 11 | 12 | def __init__(self, session: object = None): 13 | """Hive Helper. 14 | 15 | Args: 16 | session (object, optional): Interact with hive account. Defaults to None. 17 | """ 18 | self.session = session 19 | 20 | def getDeviceName(self, n_id: str): 21 | """Resolve a id into a name. 22 | 23 | Args: 24 | n_id (str): ID of a device. 25 | 26 | Returns: 27 | str: Name of device. 28 | """ 29 | try: 30 | product_name = self.session.data.products[n_id]["state"]["name"] 31 | except KeyError: 32 | product_name = False 33 | 34 | try: 35 | device_name = self.session.data.devices[n_id]["state"]["name"] 36 | except KeyError: 37 | device_name = False 38 | 39 | if product_name: 40 | return product_name 41 | elif device_name: 42 | return device_name 43 | elif n_id == "No_ID": 44 | return "Hive" 45 | else: 46 | return n_id 47 | 48 | def deviceRecovered(self, n_id: str): 49 | """Register that a device has recovered from being offline. 50 | 51 | Args: 52 | n_id (str): ID of the device. 53 | """ 54 | # name = HiveHelper.getDeviceName(n_id) 55 | if n_id in self.session.config.errorList: 56 | self.session.config.errorList.pop(n_id) 57 | 58 | def getDeviceFromID(self, n_id: str): 59 | """Get product/device data from ID. 60 | 61 | Args: 62 | n_id (str): ID of the device. 63 | 64 | Returns: 65 | dict: Device data. 66 | """ 67 | data = False 68 | try: 69 | data = self.session.devices[n_id] 70 | except KeyError: 71 | pass 72 | 73 | return data 74 | 75 | def getDeviceData(self, product: dict): 76 | """Get device from product data. 77 | 78 | Args: 79 | product (dict): Product data. 80 | 81 | Returns: 82 | [type]: Device data. 83 | """ 84 | device = product 85 | type = product["type"] 86 | if type in ("heating", "hotwater"): 87 | for aDevice in self.session.data.devices: 88 | if self.session.data.devices[aDevice]["type"] in HIVE_TYPES["Thermo"]: 89 | try: 90 | if ( 91 | product["props"]["zone"] 92 | == self.session.data.devices[aDevice]["props"]["zone"] 93 | ): 94 | device = self.session.data.devices[aDevice] 95 | except KeyError: 96 | pass 97 | elif type == "trvcontrol": 98 | trv_present = len(product["props"]["trvs"]) > 0 99 | if trv_present: 100 | device = self.session.data.devices[product["props"]["trvs"][0]] 101 | else: 102 | raise KeyError 103 | elif type == "warmwhitelight" and product["props"]["model"] == "SIREN001": 104 | device = self.session.data.devices[product["parent"]] 105 | elif type == "sense": 106 | device = self.session.data.devices[product["parent"]] 107 | else: 108 | device = self.session.data.devices[product["id"]] 109 | 110 | return device 111 | 112 | def convertMinutesToTime(self, minutes_to_convert: str): 113 | """Convert minutes string to datetime. 114 | 115 | Args: 116 | minutes_to_convert (str): minutes in string value. 117 | 118 | Returns: 119 | timedelta: time object of the minutes. 120 | """ 121 | hours_converted, minutes_converted = divmod(minutes_to_convert, 60) 122 | converted_time = datetime.datetime.strptime( 123 | str(hours_converted) + ":" + str(minutes_converted), "%H:%M" 124 | ) 125 | converted_time_string = converted_time.strftime("%H:%M") 126 | return converted_time_string 127 | 128 | def getScheduleNNL(self, hive_api_schedule: list): 129 | """Get the schedule now, next and later of a given nodes schedule. 130 | 131 | Args: 132 | hive_api_schedule (list): Schedule to parse. 133 | 134 | Returns: 135 | dict: Now, Next and later values. 136 | """ 137 | schedule_now_and_next = {} 138 | date_time_now = datetime.datetime.now() 139 | date_time_now_day_int = date_time_now.today().weekday() 140 | 141 | days_t = ( 142 | "monday", 143 | "tuesday", 144 | "wednesday", 145 | "thursday", 146 | "friday", 147 | "saturday", 148 | "sunday", 149 | ) 150 | 151 | days_rolling_list = list(days_t[date_time_now_day_int:] + days_t)[:7] 152 | 153 | full_schedule_list = [] 154 | 155 | for day_index in range(0, len(days_rolling_list)): 156 | current_day_schedule = hive_api_schedule[days_rolling_list[day_index]] 157 | current_day_schedule_sorted = sorted( 158 | current_day_schedule, 159 | key=operator.itemgetter("start"), 160 | reverse=False, 161 | ) 162 | 163 | for current_slot in range(0, len(current_day_schedule_sorted)): 164 | current_slot_custom = current_day_schedule_sorted[current_slot] 165 | 166 | slot_date = datetime.datetime.now() + datetime.timedelta(days=day_index) 167 | slot_time = self.convertMinutesToTime(current_slot_custom["start"]) 168 | slot_time_date_s = slot_date.strftime("%d-%m-%Y") + " " + slot_time 169 | slot_time_date_dt = datetime.datetime.strptime( 170 | slot_time_date_s, "%d-%m-%Y %H:%M" 171 | ) 172 | if slot_time_date_dt <= date_time_now: 173 | slot_time_date_dt = slot_time_date_dt + datetime.timedelta(days=7) 174 | 175 | current_slot_custom["Start_DateTime"] = slot_time_date_dt 176 | full_schedule_list.append(current_slot_custom) 177 | 178 | fsl_sorted = sorted( 179 | full_schedule_list, 180 | key=operator.itemgetter("Start_DateTime"), 181 | reverse=False, 182 | ) 183 | 184 | schedule_now = fsl_sorted[-1] 185 | schedule_next = fsl_sorted[0] 186 | schedule_later = fsl_sorted[1] 187 | 188 | schedule_now["Start_DateTime"] = schedule_now[ 189 | "Start_DateTime" 190 | ] - datetime.timedelta(days=7) 191 | 192 | schedule_now["End_DateTime"] = schedule_next["Start_DateTime"] 193 | schedule_next["End_DateTime"] = schedule_later["Start_DateTime"] 194 | schedule_later["End_DateTime"] = fsl_sorted[2]["Start_DateTime"] 195 | 196 | schedule_now_and_next["now"] = schedule_now 197 | schedule_now_and_next["next"] = schedule_next 198 | schedule_now_and_next["later"] = schedule_later 199 | 200 | return schedule_now_and_next 201 | 202 | def getHeatOnDemandDevice(self, device: dict): 203 | """Use TRV device to get the linked thermostat device. 204 | 205 | Args: 206 | device ([dictionary]): [The TRV device to lookup.] 207 | 208 | Returns: 209 | [dictionary]: [Gets the thermostat device linked to TRV.] 210 | """ 211 | trv = self.session.data.products.get(device["HiveID"]) 212 | thermostat = self.session.data.products.get(trv["state"]["zone"]) 213 | return thermostat 214 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/hivedataclasses.py: -------------------------------------------------------------------------------- 1 | """Device data class.""" 2 | # pylint: skip-file 3 | 4 | from dataclasses import dataclass 5 | 6 | 7 | @dataclass 8 | class Device: 9 | """Class for keeping track of an device.""" 10 | 11 | hiveID: str 12 | hiveName: str 13 | hiveType: str 14 | haType: str 15 | deviceData: dict 16 | status: dict 17 | data: dict 18 | parentDevice: str 19 | isGroup: bool 20 | device_id: str 21 | device_name: str 22 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/logger.py: -------------------------------------------------------------------------------- 1 | """Custom Logging Module.""" 2 | # pylint: skip-file 3 | import inspect 4 | from datetime import datetime 5 | 6 | 7 | class Logger: 8 | """Custom Logging Code.""" 9 | 10 | def __init__(self, session=None): 11 | """Initialise the logger class.""" 12 | self.session = session 13 | 14 | async def error(self, e="UNKNOWN"): 15 | """Process and unexpected error.""" 16 | self.session.logger.error( 17 | f"An unexpected error has occurred whilst" 18 | f" executing {inspect.stack()[1][3]}" 19 | f" with exception {e.__class__} {e}" 20 | ) 21 | 22 | async def errorCheck(self, n_id, n_type, error_type, **kwargs): 23 | """Error has occurred.""" 24 | message = None 25 | name = self.session.helper.getDeviceName(n_id) 26 | 27 | if error_type is False: 28 | message = "Device offline could not update entity - " + name 29 | if n_id not in self.session.config.errorList: 30 | self.session.logger.warning(message) 31 | self.session.config.errorList.update({n_id: datetime.now()}) 32 | elif error_type == "Failed": 33 | message = "ERROR - No data found for device - " + name 34 | if n_id not in self.session.config.errorList: 35 | self.session.logger.error(message) 36 | self.session.config.errorList.update({n_id: datetime.now()}) 37 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/helper/map.py: -------------------------------------------------------------------------------- 1 | """Dot notation for dictionary.""" 2 | # pylint: skip-file 3 | 4 | 5 | class Map(dict): 6 | """dot.notation access to dictionary attributes. 7 | 8 | Args: 9 | dict (dict): dictionary to map. 10 | """ 11 | 12 | __getattr__ = dict.get 13 | __setattr__ = dict.__setitem__ 14 | __delattr__ = dict.__delitem__ 15 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/hive.py: -------------------------------------------------------------------------------- 1 | """Start Hive Session.""" 2 | # pylint: skip-file 3 | import sys 4 | import traceback 5 | from os.path import expanduser 6 | from typing import Optional 7 | 8 | from aiohttp import ClientSession 9 | from loguru import logger 10 | 11 | from .action import HiveAction 12 | from .alarm import Alarm 13 | from .camera import Camera 14 | from .heating import Climate 15 | from .hotwater import WaterHeater 16 | from .hub import HiveHub 17 | from .light import Light 18 | from .plug import Switch 19 | from .sensor import Sensor 20 | from .session import HiveSession 21 | 22 | debug = [] 23 | home = expanduser("~") 24 | logger.add( 25 | home + "/pyhiveapi_debug.log", filter=lambda record: record["level"].name == "DEBUG" 26 | ) 27 | logger.add( 28 | home + "/pyhiveapi_info.log", filter=lambda record: record["level"].name == "INFO" 29 | ) 30 | logger.add( 31 | home + "/pyhiveapi_error.log", filter=lambda record: record["level"].name == "ERROR" 32 | ) 33 | 34 | 35 | def exception_handler(exctype, value, tb): 36 | """Custom exception handler. 37 | 38 | Args: 39 | exctype ([type]): [description] 40 | value ([type]): [description] 41 | tb ([type]): [description] 42 | """ 43 | last = len(traceback.extract_tb(tb)) - 1 44 | logger.error( 45 | f"-> \n" 46 | f"Error in {traceback.extract_tb(tb)[last].filename}\n" 47 | f"when running {traceback.extract_tb(tb)[last].name} function\n" 48 | f"on line {traceback.extract_tb(tb)[last].lineno} - " 49 | f"{traceback.extract_tb(tb)[last].line} \n" 50 | f"with vars {traceback.extract_tb(tb)[last].locals}" 51 | ) 52 | traceback.print_exc(tb) 53 | 54 | 55 | sys.excepthook = exception_handler 56 | 57 | 58 | def trace_debug(frame, event, arg): 59 | """Trace functions. 60 | 61 | Args: 62 | frame (object): The current frame being debugged. 63 | event (str): The event type 64 | arg (dict): arguments in debug function.. 65 | 66 | Returns: 67 | object: returns itself as per tracing docs 68 | """ 69 | if "pyhiveapi/" in str(frame): 70 | co = frame.f_code 71 | func_name = co.co_name 72 | func_line_no = frame.f_lineno 73 | if func_name in debug: 74 | if event == "call": 75 | func_filename = co.co_filename.rsplit("/", 1) 76 | caller = frame.f_back 77 | caller_line_no = caller.f_lineno 78 | caller_filename = caller.f_code.co_filename.rsplit("/", 1) 79 | 80 | logger.debug( 81 | f"Call to {func_name} on line {func_line_no} " 82 | f"of {func_filename[1]} from line {caller_line_no} " 83 | f"of {caller_filename[1]}" 84 | ) 85 | elif event == "return": 86 | logger.debug(f"returning {arg}") 87 | 88 | return trace_debug 89 | 90 | 91 | class Hive(HiveSession): 92 | """Hive Class. 93 | 94 | Args: 95 | HiveSession (object): Interact with Hive Account 96 | """ 97 | 98 | def __init__( 99 | self, 100 | websession: Optional[ClientSession] = None, 101 | username: str = None, 102 | password: str = None, 103 | ): 104 | """Generate a Hive session. 105 | 106 | Args: 107 | websession (Optional[ClientSession], optional): This is a websession that can be used for the api. Defaults to None. 108 | username (str, optional): This is the Hive username used for login. Defaults to None. 109 | password (str, optional): This is the Hive password used for login. Defaults to None. 110 | """ 111 | super().__init__(username, password, websession) 112 | self.session = self 113 | self.action = HiveAction(self.session) 114 | self.alarm = Alarm(self.session) 115 | self.camera = Camera(self.session) 116 | self.heating = Climate(self.session) 117 | self.hotwater = WaterHeater(self.session) 118 | self.hub = HiveHub(self.session) 119 | self.light = Light(self.session) 120 | self.switch = Switch(self.session) 121 | self.sensor = Sensor(self.session) 122 | self.logger = logger 123 | if debug: 124 | sys.settrace(trace_debug) 125 | 126 | def setDebugging(self, debugger: list): 127 | """Set function to debug. 128 | 129 | Args: 130 | debugger (list): a list of functions to debug 131 | 132 | Returns: 133 | object: Returns traceback object. 134 | """ 135 | global debug 136 | debug = debugger 137 | if debug: 138 | return sys.settrace(trace_debug) 139 | return sys.settrace(None) 140 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/hotwater.py: -------------------------------------------------------------------------------- 1 | """Hive Hotwater Module.""" 2 | # pylint: skip-file 3 | 4 | from .helper.const import HIVETOHA 5 | 6 | 7 | class HiveHotwater: 8 | """Hive Hotwater Code. 9 | 10 | Returns: 11 | object: Hotwater Object. 12 | """ 13 | 14 | hotwaterType = "Hotwater" 15 | 16 | async def getMode(self, device: dict): 17 | """Get hotwater current mode. 18 | 19 | Args: 20 | device (dict): Device to get the mode for. 21 | 22 | Returns: 23 | str: Return mode. 24 | """ 25 | state = None 26 | final = None 27 | 28 | try: 29 | data = self.session.data.products[device["hiveID"]] 30 | state = data["state"]["mode"] 31 | if state == "BOOST": 32 | state = data["props"]["previous"]["mode"] 33 | final = HIVETOHA[self.hotwaterType].get(state, state) 34 | except KeyError as e: 35 | await self.session.log.error(e) 36 | 37 | return final 38 | 39 | @staticmethod 40 | async def getOperationModes(): 41 | """Get heating list of possible modes. 42 | 43 | Returns: 44 | list: Return list of operation modes. 45 | """ 46 | return ["SCHEDULE", "ON", "OFF"] 47 | 48 | async def getBoost(self, device: dict): 49 | """Get hot water current boost status. 50 | 51 | Args: 52 | device (dict): Device to get boost status for 53 | 54 | Returns: 55 | str: Return boost status. 56 | """ 57 | state = None 58 | final = None 59 | 60 | try: 61 | data = self.session.data.products[device["hiveID"]] 62 | state = data["state"]["boost"] 63 | final = HIVETOHA["Boost"].get(state, "ON") 64 | except KeyError as e: 65 | await self.session.log.error(e) 66 | 67 | return final 68 | 69 | async def getBoostTime(self, device: dict): 70 | """Get hotwater boost time remaining. 71 | 72 | Args: 73 | device (dict): Device to get boost time for. 74 | 75 | Returns: 76 | str: Return time remaining on the boost. 77 | """ 78 | state = None 79 | if await self.getBoost(device) == "ON": 80 | try: 81 | data = self.session.data.products[device["hiveID"]] 82 | state = data["state"]["boost"] 83 | except KeyError as e: 84 | await self.session.log.error(e) 85 | 86 | return state 87 | 88 | async def getState(self, device: dict): 89 | """Get hot water current state. 90 | 91 | Args: 92 | device (dict): Device to get the state for. 93 | 94 | Returns: 95 | str: return state of device. 96 | """ 97 | state = None 98 | final = None 99 | 100 | try: 101 | data = self.session.data.products[device["hiveID"]] 102 | state = data["state"]["status"] 103 | mode_current = await self.getMode(device) 104 | if mode_current == "SCHEDULE": 105 | if await self.getBoost(device) == "ON": 106 | state = "ON" 107 | else: 108 | snan = self.session.helper.getScheduleNNL(data["state"]["schedule"]) 109 | state = snan["now"]["value"]["status"] 110 | 111 | final = HIVETOHA[self.hotwaterType].get(state, state) 112 | except KeyError as e: 113 | await self.session.log.error(e) 114 | 115 | return final 116 | 117 | async def setMode(self, device: dict, new_mode: str): 118 | """Set hot water mode. 119 | 120 | Args: 121 | device (dict): device to update mode. 122 | new_mode (str): Mode to set the device to. 123 | 124 | Returns: 125 | boolean: return True/False if boost was successful. 126 | """ 127 | final = False 128 | 129 | if device["hiveID"] in self.session.data.products: 130 | await self.session.hiveRefreshTokens() 131 | data = self.session.data.products[device["hiveID"]] 132 | resp = await self.session.api.setState( 133 | data["type"], device["hiveID"], mode=new_mode 134 | ) 135 | if resp["original"] == 200: 136 | final = True 137 | await self.session.getDevices(device["hiveID"]) 138 | 139 | return final 140 | 141 | async def setBoostOn(self, device: dict, mins: int): 142 | """Turn hot water boost on. 143 | 144 | Args: 145 | device (dict): Deice to boost. 146 | mins (int): Number of minutes to boost it for. 147 | 148 | Returns: 149 | boolean: return True/False if boost was successful. 150 | """ 151 | final = False 152 | 153 | if ( 154 | int(mins) > 0 155 | and device["hiveID"] in self.session.data.products 156 | and device["deviceData"]["online"] 157 | ): 158 | await self.session.hiveRefreshTokens() 159 | data = self.session.data.products[device["hiveID"]] 160 | resp = await self.session.api.setState( 161 | data["type"], device["hiveID"], mode="BOOST", boost=mins 162 | ) 163 | if resp["original"] == 200: 164 | final = True 165 | await self.session.getDevices(device["hiveID"]) 166 | 167 | return final 168 | 169 | async def setBoostOff(self, device: dict): 170 | """Turn hot water boost off. 171 | 172 | Args: 173 | device (dict): device to set boost off 174 | 175 | Returns: 176 | boolean: return True/False if boost was successful. 177 | """ 178 | final = False 179 | 180 | if ( 181 | device["hiveID"] in self.session.data.products 182 | and await self.getBoost(device) == "ON" 183 | and device["deviceData"]["online"] 184 | ): 185 | await self.session.hiveRefreshTokens() 186 | data = self.session.data.products[device["hiveID"]] 187 | prev_mode = data["props"]["previous"]["mode"] 188 | resp = await self.session.api.setState( 189 | data["type"], device["hiveID"], mode=prev_mode 190 | ) 191 | if resp["original"] == 200: 192 | await self.session.getDevices(device["hiveID"]) 193 | final = True 194 | 195 | return final 196 | 197 | 198 | class WaterHeater(HiveHotwater): 199 | """Water heater class. 200 | 201 | Args: 202 | Hotwater (object): Hotwater class. 203 | """ 204 | 205 | def __init__(self, session: object = None): 206 | """Initialise water heater. 207 | 208 | Args: 209 | session (object, optional): Session to interact with account. Defaults to None. 210 | """ 211 | self.session = session 212 | 213 | async def getWaterHeater(self, device: dict): 214 | """Update water heater device. 215 | 216 | Args: 217 | device (dict): device to update. 218 | 219 | Returns: 220 | dict: Updated device. 221 | """ 222 | device["deviceData"].update( 223 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 224 | ) 225 | 226 | if device["deviceData"]["online"]: 227 | 228 | dev_data = {} 229 | self.session.helper.deviceRecovered(device["device_id"]) 230 | data = self.session.data.devices[device["device_id"]] 231 | dev_data = { 232 | "hiveID": device["hiveID"], 233 | "hiveName": device["hiveName"], 234 | "hiveType": device["hiveType"], 235 | "haName": device["haName"], 236 | "haType": device["haType"], 237 | "device_id": device["device_id"], 238 | "device_name": device["device_name"], 239 | "status": {"current_operation": await self.getMode(device)}, 240 | "deviceData": data.get("props", None), 241 | "parentDevice": data.get("parent", None), 242 | "custom": device.get("custom", None), 243 | "attributes": await self.session.attr.stateAttributes( 244 | device["device_id"], device["hiveType"] 245 | ), 246 | } 247 | 248 | self.session.devices.update({device["hiveID"]: dev_data}) 249 | return self.session.devices[device["hiveID"]] 250 | else: 251 | await self.session.log.errorCheck( 252 | device["device_id"], "ERROR", device["deviceData"]["online"] 253 | ) 254 | return device 255 | 256 | async def getScheduleNowNextLater(self, device: dict): 257 | """Hive get hotwater schedule now, next and later. 258 | 259 | Args: 260 | device (dict): device to get schedule for. 261 | 262 | Returns: 263 | dict: return now, next and later schedule. 264 | """ 265 | state = None 266 | 267 | try: 268 | mode_current = await self.getMode(device) 269 | if mode_current == "SCHEDULE": 270 | data = self.session.data.products[device["hiveID"]] 271 | state = self.session.helper.getScheduleNNL(data["state"]["schedule"]) 272 | except KeyError as e: 273 | await self.session.log.error(e) 274 | 275 | return state 276 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/hub.py: -------------------------------------------------------------------------------- 1 | """Hive Hub Module.""" 2 | # pylint: skip-file 3 | from .helper.const import HIVETOHA 4 | 5 | 6 | class HiveHub: 7 | """Hive hub. 8 | 9 | Returns: 10 | object: Returns a hub object. 11 | """ 12 | 13 | hubType = "Hub" 14 | logType = "Sensor" 15 | 16 | def __init__(self, session: object = None): 17 | """Initialise hub. 18 | 19 | Args: 20 | session (object, optional): session to interact with Hive account. Defaults to None. 21 | """ 22 | self.session = session 23 | 24 | async def getSmokeStatus(self, device: dict): 25 | """Get the hub smoke status. 26 | 27 | Args: 28 | device (dict): device to get status for 29 | 30 | Returns: 31 | str: Return smoke status. 32 | """ 33 | state = None 34 | final = None 35 | 36 | try: 37 | data = self.session.data.products[device["hiveID"]] 38 | state = data["props"]["sensors"]["SMOKE_CO"]["active"] 39 | final = HIVETOHA[self.hubType]["Smoke"].get(state, state) 40 | except KeyError as e: 41 | await self.session.log.error(e) 42 | 43 | return final 44 | 45 | async def getDogBarkStatus(self, device: dict): 46 | """Get dog bark status. 47 | 48 | Args: 49 | device (dict): Device to get status for. 50 | 51 | Returns: 52 | str: Return status. 53 | """ 54 | state = None 55 | final = None 56 | 57 | try: 58 | data = self.session.data.products[device["hiveID"]] 59 | state = data["props"]["sensors"]["DOG_BARK"]["active"] 60 | final = HIVETOHA[self.hubType]["Dog"].get(state, state) 61 | except KeyError as e: 62 | await self.session.log.error(e) 63 | 64 | return final 65 | 66 | async def getGlassBreakStatus(self, device: dict): 67 | """Get the glass detected status from the Hive hub. 68 | 69 | Args: 70 | device (dict): Device to get status for. 71 | 72 | Returns: 73 | str: Return status. 74 | """ 75 | state = None 76 | final = None 77 | 78 | try: 79 | data = self.session.data.products[device["hiveID"]] 80 | state = data["props"]["sensors"]["GLASS_BREAK"]["active"] 81 | final = HIVETOHA[self.hubType]["Glass"].get(state, state) 82 | except KeyError as e: 83 | await self.session.log.error(e) 84 | 85 | return final 86 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/light.py: -------------------------------------------------------------------------------- 1 | """Hive Light Module.""" 2 | # pylint: skip-file 3 | import colorsys 4 | 5 | from .helper.const import HIVETOHA 6 | 7 | 8 | class HiveLight: 9 | """Hive Light Code. 10 | 11 | Returns: 12 | object: Hivelight 13 | """ 14 | 15 | lightType = "Light" 16 | 17 | async def getState(self, device: dict): 18 | """Get light current state. 19 | 20 | Args: 21 | device (dict): Device to get the state of. 22 | 23 | Returns: 24 | str: State of the light. 25 | """ 26 | state = None 27 | final = None 28 | 29 | try: 30 | data = self.session.data.products[device["hiveID"]] 31 | state = data["state"]["status"] 32 | final = HIVETOHA[self.lightType].get(state, state) 33 | except KeyError as e: 34 | await self.session.log.error(e) 35 | 36 | return final 37 | 38 | async def getBrightness(self, device: dict): 39 | """Get light current brightness. 40 | 41 | Args: 42 | device (dict): Device to get the brightness of. 43 | 44 | Returns: 45 | int: Brightness value. 46 | """ 47 | state = None 48 | final = None 49 | 50 | try: 51 | data = self.session.data.products[device["hiveID"]] 52 | state = data["state"]["brightness"] 53 | final = (state / 100) * 255 54 | except KeyError as e: 55 | await self.session.log.error(e) 56 | 57 | return final 58 | 59 | async def getMinColorTemp(self, device: dict): 60 | """Get light minimum color temperature. 61 | 62 | Args: 63 | device (dict): Device to get min colour temp for. 64 | 65 | Returns: 66 | int: Min color temperature. 67 | """ 68 | state = None 69 | final = None 70 | 71 | try: 72 | data = self.session.data.products[device["hiveID"]] 73 | state = data["props"]["colourTemperature"]["max"] 74 | final = round((1 / state) * 1000000) 75 | except KeyError as e: 76 | await self.session.log.error(e) 77 | 78 | return final 79 | 80 | async def getMaxColorTemp(self, device: dict): 81 | """Get light maximum color temperature. 82 | 83 | Args: 84 | device (dict): Device to get max colour temp for. 85 | 86 | Returns: 87 | int: Min color temperature. 88 | """ 89 | state = None 90 | final = None 91 | 92 | try: 93 | data = self.session.data.products[device["hiveID"]] 94 | state = data["props"]["colourTemperature"]["min"] 95 | final = round((1 / state) * 1000000) 96 | except KeyError as e: 97 | await self.session.log.error(e) 98 | 99 | return final 100 | 101 | async def getColorTemp(self, device: dict): 102 | """Get light current color temperature. 103 | 104 | Args: 105 | device (dict): Device to get colour temp for. 106 | 107 | Returns: 108 | int: Current Color Temp. 109 | """ 110 | state = None 111 | final = None 112 | 113 | try: 114 | data = self.session.data.products[device["hiveID"]] 115 | state = data["state"]["colourTemperature"] 116 | final = round((1 / state) * 1000000) 117 | except KeyError as e: 118 | await self.session.log.error(e) 119 | 120 | return final 121 | 122 | async def getColor(self, device: dict): 123 | """Get light current colour. 124 | 125 | Args: 126 | device (dict): Device to get color for. 127 | 128 | Returns: 129 | tuple: RGB values for the color. 130 | """ 131 | state = None 132 | final = None 133 | 134 | try: 135 | data = self.session.data.products[device["hiveID"]] 136 | state = [ 137 | (data["state"]["hue"]) / 360, 138 | (data["state"]["saturation"]) / 100, 139 | (data["state"]["value"]) / 100, 140 | ] 141 | final = tuple( 142 | int(i * 255) for i in colorsys.hsv_to_rgb(state[0], state[1], state[2]) 143 | ) 144 | except KeyError as e: 145 | await self.session.log.error(e) 146 | 147 | return final 148 | 149 | async def getColorMode(self, device: dict): 150 | """Get Colour Mode. 151 | 152 | Args: 153 | device (dict): Device to get the color mode for. 154 | 155 | Returns: 156 | str: Colour mode. 157 | """ 158 | state = None 159 | 160 | try: 161 | data = self.session.data.products[device["hiveID"]] 162 | state = data["state"]["colourMode"] 163 | except KeyError as e: 164 | await self.session.log.error(e) 165 | 166 | return state 167 | 168 | async def setStatusOff(self, device: dict): 169 | """Set light to turn off. 170 | 171 | Args: 172 | device (dict): Device to turn off. 173 | 174 | Returns: 175 | boolean: True/False if successful. 176 | """ 177 | final = False 178 | 179 | if ( 180 | device["hiveID"] in self.session.data.products 181 | and device["deviceData"]["online"] 182 | ): 183 | await self.session.hiveRefreshTokens() 184 | data = self.session.data.products[device["hiveID"]] 185 | resp = await self.session.api.setState( 186 | data["type"], device["hiveID"], status="OFF" 187 | ) 188 | 189 | if resp["original"] == 200: 190 | final = True 191 | await self.session.getDevices(device["hiveID"]) 192 | 193 | return final 194 | 195 | async def setStatusOn(self, device: dict): 196 | """Set light to turn on. 197 | 198 | Args: 199 | device (dict): Device to turn on. 200 | 201 | Returns: 202 | boolean: True/False if successful. 203 | """ 204 | final = False 205 | 206 | if ( 207 | device["hiveID"] in self.session.data.products 208 | and device["deviceData"]["online"] 209 | ): 210 | await self.session.hiveRefreshTokens() 211 | data = self.session.data.products[device["hiveID"]] 212 | 213 | resp = await self.session.api.setState( 214 | data["type"], device["hiveID"], status="ON" 215 | ) 216 | if resp["original"] == 200: 217 | final = True 218 | await self.session.getDevices(device["hiveID"]) 219 | 220 | return final 221 | 222 | async def setBrightness(self, device: dict, n_brightness: int): 223 | """Set brightness of the light. 224 | 225 | Args: 226 | device (dict): Device to set brightness for. 227 | n_brightness (int): Brightness value 228 | 229 | Returns: 230 | boolean: True/False if successful. 231 | """ 232 | final = False 233 | 234 | if ( 235 | device["hiveID"] in self.session.data.products 236 | and device["deviceData"]["online"] 237 | ): 238 | await self.session.hiveRefreshTokens() 239 | data = self.session.data.products[device["hiveID"]] 240 | resp = await self.session.api.setState( 241 | data["type"], 242 | device["hiveID"], 243 | status="ON", 244 | brightness=n_brightness, 245 | ) 246 | if resp["original"] == 200: 247 | final = True 248 | await self.session.getDevices(device["hiveID"]) 249 | 250 | return final 251 | 252 | async def setColorTemp(self, device: dict, color_temp: int): 253 | """Set light to turn on. 254 | 255 | Args: 256 | device (dict): Device to set color temp for. 257 | color_temp (int): Color temp value. 258 | 259 | Returns: 260 | boolean: True/False if successful. 261 | """ 262 | final = False 263 | 264 | if ( 265 | device["hiveID"] in self.session.data.products 266 | and device["deviceData"]["online"] 267 | ): 268 | await self.session.hiveRefreshTokens() 269 | data = self.session.data.products[device["hiveID"]] 270 | 271 | if data["type"] == "tuneablelight": 272 | resp = await self.session.api.setState( 273 | data["type"], 274 | device["hiveID"], 275 | colourTemperature=color_temp, 276 | ) 277 | else: 278 | resp = await self.session.api.setState( 279 | data["type"], 280 | device["hiveID"], 281 | colourMode="WHITE", 282 | colourTemperature=color_temp, 283 | ) 284 | 285 | if resp["original"] == 200: 286 | final = True 287 | await self.session.getDevices(device["hiveID"]) 288 | 289 | return final 290 | 291 | async def setColor(self, device: dict, new_color: list): 292 | """Set light to turn on. 293 | 294 | Args: 295 | device (dict): Device to set color for. 296 | new_color (list): HSV value to set the light to. 297 | 298 | Returns: 299 | boolean: True/False if successful. 300 | """ 301 | final = False 302 | 303 | if ( 304 | device["hiveID"] in self.session.data.products 305 | and device["deviceData"]["online"] 306 | ): 307 | await self.session.hiveRefreshTokens() 308 | data = self.session.data.products[device["hiveID"]] 309 | 310 | resp = await self.session.api.setState( 311 | data["type"], 312 | device["hiveID"], 313 | colourMode="COLOUR", 314 | hue=str(new_color[0]), 315 | saturation=str(new_color[1]), 316 | value=str(new_color[2]), 317 | ) 318 | if resp["original"] == 200: 319 | final = True 320 | await self.session.getDevices(device["hiveID"]) 321 | 322 | return final 323 | 324 | 325 | class Light(HiveLight): 326 | """Home Assistant Light Code. 327 | 328 | Args: 329 | HiveLight (object): HiveLight Code. 330 | """ 331 | 332 | def __init__(self, session: object = None): 333 | """Initialise light. 334 | 335 | Args: 336 | session (object, optional): Used to interact with the hive account. Defaults to None. 337 | """ 338 | self.session = session 339 | 340 | async def getLight(self, device: dict): 341 | """Get light data. 342 | 343 | Args: 344 | device (dict): Device to update. 345 | 346 | Returns: 347 | dict: Updated device. 348 | """ 349 | device["deviceData"].update( 350 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 351 | ) 352 | dev_data = {} 353 | 354 | if device["deviceData"]["online"]: 355 | self.session.helper.deviceRecovered(device["device_id"]) 356 | data = self.session.data.devices[device["device_id"]] 357 | dev_data = { 358 | "hiveID": device["hiveID"], 359 | "hiveName": device["hiveName"], 360 | "hiveType": device["hiveType"], 361 | "haName": device["haName"], 362 | "haType": device["haType"], 363 | "device_id": device["device_id"], 364 | "device_name": device["device_name"], 365 | "status": { 366 | "state": await self.getState(device), 367 | "brightness": await self.getBrightness(device), 368 | }, 369 | "deviceData": data.get("props", None), 370 | "parentDevice": data.get("parent", None), 371 | "custom": device.get("custom", None), 372 | "attributes": await self.session.attr.stateAttributes( 373 | device["device_id"], device["hiveType"] 374 | ), 375 | } 376 | 377 | if device["hiveType"] in ("tuneablelight", "colourtuneablelight"): 378 | dev_data.update( 379 | { 380 | "min_mireds": await self.getMinColorTemp(device), 381 | "max_mireds": await self.getMaxColorTemp(device), 382 | } 383 | ) 384 | dev_data["status"].update( 385 | {"color_temp": await self.getColorTemp(device)} 386 | ) 387 | if device["hiveType"] == "colourtuneablelight": 388 | mode = await self.getColorMode(device) 389 | if mode == "COLOUR": 390 | dev_data["status"].update( 391 | { 392 | "hs_color": await self.getColor(device), 393 | "mode": await self.getColorMode(device), 394 | } 395 | ) 396 | else: 397 | dev_data["status"].update( 398 | { 399 | "mode": await self.getColorMode(device), 400 | } 401 | ) 402 | 403 | self.session.devices.update({device["hiveID"]: dev_data}) 404 | return self.session.devices[device["hiveID"]] 405 | else: 406 | await self.session.log.errorCheck( 407 | device["device_id"], "ERROR", device["deviceData"]["online"] 408 | ) 409 | return device 410 | 411 | async def turnOn(self, device: dict, brightness: int, color_temp: int, color: list): 412 | """Set light to turn on. 413 | 414 | Args: 415 | device (dict): Device to turn on 416 | brightness (int): Brightness value to set the light to. 417 | color_temp (int): Color Temp value to set the light to. 418 | color (list): colour values to set the light to. 419 | 420 | Returns: 421 | boolean: True/False if successful. 422 | """ 423 | if brightness is not None: 424 | return await self.setBrightness(device, brightness) 425 | if color_temp is not None: 426 | return await self.setColorTemp(device, color_temp) 427 | if color is not None: 428 | return await self.setColor(device, color) 429 | 430 | return await self.setStatusOn(device) 431 | 432 | async def turnOff(self, device: dict): 433 | """Set light to turn off. 434 | 435 | Args: 436 | device (dict): Device to be turned off. 437 | 438 | Returns: 439 | boolean: True/False if successful. 440 | """ 441 | return await self.setStatusOff(device) 442 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/plug.py: -------------------------------------------------------------------------------- 1 | """Hive Switch Module.""" 2 | # pylint: skip-file 3 | from .helper.const import HIVETOHA 4 | 5 | 6 | class HiveSmartPlug: 7 | """Plug Device. 8 | 9 | Returns: 10 | object: Returns Plug object 11 | """ 12 | 13 | plugType = "Switch" 14 | 15 | async def getState(self, device: dict): 16 | """Get smart plug state. 17 | 18 | Args: 19 | device (dict): Device to get the plug state for. 20 | 21 | Returns: 22 | boolean: Returns True or False based on if the plug is on 23 | """ 24 | state = None 25 | 26 | try: 27 | data = self.session.data.products[device["hiveID"]] 28 | state = data["state"]["status"] 29 | state = HIVETOHA["Switch"].get(state, state) 30 | except KeyError as e: 31 | await self.session.log.error(e) 32 | 33 | return state 34 | 35 | async def getPowerUsage(self, device: dict): 36 | """Get smart plug current power usage. 37 | 38 | Args: 39 | device (dict): [description] 40 | 41 | Returns: 42 | [type]: [description] 43 | """ 44 | state = None 45 | 46 | try: 47 | data = self.session.data.products[device["hiveID"]] 48 | state = data["props"]["powerConsumption"] 49 | except KeyError as e: 50 | await self.session.log.error(e) 51 | 52 | return state 53 | 54 | async def setStatusOn(self, device: dict): 55 | """Set smart plug to turn on. 56 | 57 | Args: 58 | device (dict): Device to switch on. 59 | 60 | Returns: 61 | boolean: True/False if successful 62 | """ 63 | final = False 64 | 65 | if ( 66 | device["hiveID"] in self.session.data.products 67 | and device["deviceData"]["online"] 68 | ): 69 | await self.session.hiveRefreshTokens() 70 | data = self.session.data.products[device["hiveID"]] 71 | resp = await self.session.api.setState( 72 | data["type"], data["id"], status="ON" 73 | ) 74 | if resp["original"] == 200: 75 | final = True 76 | await self.session.getDevices(device["hiveID"]) 77 | 78 | return final 79 | 80 | async def setStatusOff(self, device: dict): 81 | """Set smart plug to turn off. 82 | 83 | Args: 84 | device (dict): Device to switch off. 85 | 86 | Returns: 87 | boolean: True/False if successful 88 | """ 89 | final = False 90 | 91 | if ( 92 | device["hiveID"] in self.session.data.products 93 | and device["deviceData"]["online"] 94 | ): 95 | await self.session.hiveRefreshTokens() 96 | data = self.session.data.products[device["hiveID"]] 97 | resp = await self.session.api.setState( 98 | data["type"], data["id"], status="OFF" 99 | ) 100 | if resp["original"] == 200: 101 | final = True 102 | await self.session.getDevices(device["hiveID"]) 103 | 104 | return final 105 | 106 | 107 | class Switch(HiveSmartPlug): 108 | """Home Assistant switch class. 109 | 110 | Args: 111 | SmartPlug (Class): Initialises the Smartplug Class. 112 | """ 113 | 114 | def __init__(self, session: object): 115 | """Initialise switch. 116 | 117 | Args: 118 | session (object): This is the session object to interact with the current session. 119 | """ 120 | self.session = session 121 | 122 | async def getSwitch(self, device: dict): 123 | """Home assistant wrapper to get switch device. 124 | 125 | Args: 126 | device (dict): Device to be update. 127 | 128 | Returns: 129 | dict: Return device after update is complete. 130 | """ 131 | device["deviceData"].update( 132 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 133 | ) 134 | dev_data = {} 135 | 136 | if device["deviceData"]["online"]: 137 | self.session.helper.deviceRecovered(device["device_id"]) 138 | data = self.session.data.devices[device["device_id"]] 139 | dev_data = { 140 | "hiveID": device["hiveID"], 141 | "hiveName": device["hiveName"], 142 | "hiveType": device["hiveType"], 143 | "haName": device["haName"], 144 | "haType": device["haType"], 145 | "device_id": device["device_id"], 146 | "device_name": device["device_name"], 147 | "status": { 148 | "state": await self.getSwitchState(device), 149 | }, 150 | "deviceData": data.get("props", None), 151 | "parentDevice": data.get("parent", None), 152 | "custom": device.get("custom", None), 153 | "attributes": {}, 154 | } 155 | 156 | if device["hiveType"] == "activeplug": 157 | dev_data.update( 158 | { 159 | "status": { 160 | "state": dev_data["status"]["state"], 161 | "power_usage": await self.getPowerUsage(device), 162 | }, 163 | "attributes": await self.session.attr.stateAttributes( 164 | device["device_id"], device["hiveType"] 165 | ), 166 | } 167 | ) 168 | 169 | self.session.devices.update({device["hiveID"]: dev_data}) 170 | return self.session.devices[device["hiveID"]] 171 | else: 172 | await self.session.log.errorCheck( 173 | device["device_id"], "ERROR", device["deviceData"]["online"] 174 | ) 175 | return device 176 | 177 | async def getSwitchState(self, device: dict): 178 | """Home Assistant wrapper to get updated switch state. 179 | 180 | Args: 181 | device (dict): Device to get state for 182 | 183 | Returns: 184 | boolean: Return True or False for the state. 185 | """ 186 | if device["hiveType"] == "Heating_Heat_On_Demand": 187 | return await self.session.heating.getHeatOnDemand(device) 188 | else: 189 | return await self.getState(device) 190 | 191 | async def turnOn(self, device: dict): 192 | """Home Assisatnt wrapper for turning switch on. 193 | 194 | Args: 195 | device (dict): Device to switch on. 196 | 197 | Returns: 198 | function: Calls relevant function. 199 | """ 200 | if device["hiveType"] == "Heating_Heat_On_Demand": 201 | return await self.session.heating.setHeatOnDemand(device, "ENABLED") 202 | else: 203 | return await self.setStatusOn(device) 204 | 205 | async def turnOff(self, device: dict): 206 | """Home Assisatnt wrapper for turning switch off. 207 | 208 | Args: 209 | device (dict): Device to switch off. 210 | 211 | Returns: 212 | function: Calls relevant function. 213 | """ 214 | if device["hiveType"] == "Heating_Heat_On_Demand": 215 | return await self.session.heating.setHeatOnDemand(device, "DISABLED") 216 | else: 217 | return await self.setStatusOff(device) 218 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/sensor.py: -------------------------------------------------------------------------------- 1 | """Hive Sensor Module.""" 2 | # pylint: skip-file 3 | from .helper.const import HIVE_TYPES, HIVETOHA, sensor_commands 4 | 5 | 6 | class HiveSensor: 7 | """Hive Sensor Code.""" 8 | 9 | sensorType = "Sensor" 10 | 11 | async def getState(self, device: dict): 12 | """Get sensor state. 13 | 14 | Args: 15 | device (dict): Device to get state off. 16 | 17 | Returns: 18 | str: State of device. 19 | """ 20 | state = None 21 | final = None 22 | 23 | try: 24 | data = self.session.data.products[device["hiveID"]] 25 | if data["type"] == "contactsensor": 26 | state = data["props"]["status"] 27 | final = HIVETOHA[self.sensorType].get(state, state) 28 | elif data["type"] == "motionsensor": 29 | final = data["props"]["motion"]["status"] 30 | except KeyError as e: 31 | await self.session.log.error(e) 32 | 33 | return final 34 | 35 | async def online(self, device: dict): 36 | """Get the online status of the Hive hub. 37 | 38 | Args: 39 | device (dict): Device to get the state of. 40 | 41 | Returns: 42 | boolean: True/False if the device is online. 43 | """ 44 | state = None 45 | final = None 46 | 47 | try: 48 | data = self.session.data.devices[device["device_id"]] 49 | state = data["props"]["online"] 50 | final = HIVETOHA[self.sensorType].get(state, state) 51 | except KeyError as e: 52 | await self.session.log.error(e) 53 | 54 | return final 55 | 56 | 57 | class Sensor(HiveSensor): 58 | """Home Assisatnt sensor code. 59 | 60 | Args: 61 | HiveSensor (object): Hive sensor code. 62 | """ 63 | 64 | def __init__(self, session: object = None): 65 | """Initialise sensor. 66 | 67 | Args: 68 | session (object, optional): session to interact with Hive account. Defaults to None. 69 | """ 70 | self.session = session 71 | 72 | async def getSensor(self, device: dict): 73 | """Gets updated sensor data. 74 | 75 | Args: 76 | device (dict): Device to update. 77 | 78 | Returns: 79 | dict: Updated device. 80 | """ 81 | device["deviceData"].update( 82 | {"online": await self.session.attr.onlineOffline(device["device_id"])} 83 | ) 84 | data = {} 85 | 86 | if device["deviceData"]["online"] or device["hiveType"] in ( 87 | "Availability", 88 | "Connectivity", 89 | ): 90 | if device["hiveType"] not in ("Availability", "Connectivity"): 91 | self.session.helper.deviceRecovered(device["device_id"]) 92 | 93 | dev_data = {} 94 | dev_data = { 95 | "hiveID": device["hiveID"], 96 | "hiveName": device["hiveName"], 97 | "hiveType": device["hiveType"], 98 | "haName": device["haName"], 99 | "haType": device["haType"], 100 | "device_id": device.get("device_id", None), 101 | "device_name": device.get("device_name", None), 102 | "deviceData": {}, 103 | "custom": device.get("custom", None), 104 | } 105 | 106 | if device["device_id"] in self.session.data.devices: 107 | data = self.session.data.devices.get(device["device_id"], {}) 108 | elif device["hiveID"] in self.session.data.products: 109 | data = self.session.data.products.get(device["hiveID"], {}) 110 | 111 | if ( 112 | dev_data["hiveType"] in sensor_commands 113 | or dev_data.get("custom", None) in sensor_commands 114 | ): 115 | code = sensor_commands.get( 116 | dev_data["hiveType"], 117 | sensor_commands.get(dev_data["custom"]), 118 | ) 119 | dev_data.update( 120 | { 121 | "status": {"state": await eval(code)}, 122 | "deviceData": data.get("props", None), 123 | "parentDevice": data.get("parent", None), 124 | } 125 | ) 126 | elif device["hiveType"] in HIVE_TYPES["Sensor"]: 127 | data = self.session.data.devices.get(device["hiveID"], {}) 128 | dev_data.update( 129 | { 130 | "status": {"state": await self.getState(device)}, 131 | "deviceData": data.get("props", None), 132 | "parentDevice": data.get("parent", None), 133 | "attributes": await self.session.attr.stateAttributes( 134 | device["device_id"], device["hiveType"] 135 | ), 136 | } 137 | ) 138 | 139 | self.session.devices.update({device["hiveID"]: dev_data}) 140 | return self.session.devices[device["hiveID"]] 141 | else: 142 | await self.session.log.errorCheck( 143 | device["device_id"], "ERROR", device["deviceData"]["online"] 144 | ) 145 | return device 146 | -------------------------------------------------------------------------------- /pyhiveapi/apyhiveapi/session.py: -------------------------------------------------------------------------------- 1 | """Hive Session Module.""" 2 | # pylint: skip-file 3 | import asyncio 4 | import copy 5 | import json 6 | import operator 7 | import os 8 | import time 9 | from datetime import datetime, timedelta 10 | 11 | from aiohttp.web import HTTPException 12 | from apyhiveapi import API, Auth 13 | 14 | from .device_attributes import HiveAttributes 15 | from .helper.const import ACTIONS, DEVICES, HIVE_TYPES, PRODUCTS 16 | from .helper.hive_exceptions import ( 17 | HiveApiError, 18 | HiveFailedToRefreshTokens, 19 | HiveInvalid2FACode, 20 | HiveInvalidDeviceAuthentication, 21 | HiveInvalidPassword, 22 | HiveInvalidUsername, 23 | HiveReauthRequired, 24 | HiveUnknownConfiguration, 25 | NoApiToken, 26 | ) 27 | from .helper.hive_helper import HiveHelper 28 | from .helper.logger import Logger 29 | from .helper.map import Map 30 | 31 | 32 | class HiveSession: 33 | """Hive Session Code. 34 | 35 | Raises: 36 | HiveUnknownConfiguration: Unknown configuration. 37 | HTTPException: HTTP error has occurred. 38 | HiveApiError: Hive has retuend an error code. 39 | HiveReauthRequired: Tokens have expired and reauthentiction is required. 40 | 41 | Returns: 42 | object: Session object. 43 | """ 44 | 45 | sessionType = "Session" 46 | 47 | def __init__( 48 | self, 49 | username: str = None, 50 | password: str = None, 51 | websession: object = None, 52 | ): 53 | """Initialise the base variable values. 54 | 55 | Args: 56 | username (str, optional): Hive username. Defaults to None. 57 | password (str, optional): Hive Password. Defaults to None. 58 | websession (object, optional): Websession for api calls. Defaults to None. 59 | """ 60 | self.auth = Auth( 61 | username=username, 62 | password=password, 63 | ) 64 | self.api = API(hiveSession=self, websession=websession) 65 | self.helper = HiveHelper(self) 66 | self.attr = HiveAttributes(self) 67 | self.log = Logger(self) 68 | self.updateLock = asyncio.Lock() 69 | self.tokens = Map( 70 | { 71 | "tokenData": {}, 72 | "tokenCreated": datetime.now() - timedelta(seconds=4000), 73 | "tokenExpiry": timedelta(seconds=3600), 74 | } 75 | ) 76 | self.config = Map( 77 | { 78 | "alarm": False, 79 | "battery": [], 80 | "camera": False, 81 | "errorList": {}, 82 | "file": False, 83 | "homeID": None, 84 | "lastUpdated": datetime.now(), 85 | "mode": [], 86 | "scanInterval": timedelta(seconds=120), 87 | "userID": None, 88 | "username": username, 89 | } 90 | ) 91 | self.data = Map( 92 | { 93 | "products": {}, 94 | "devices": {}, 95 | "actions": {}, 96 | "user": {}, 97 | "minMax": {}, 98 | "alarm": {}, 99 | "camera": {}, 100 | } 101 | ) 102 | self.devices = {} 103 | self.deviceList = {} 104 | 105 | def openFile(self, file: str): 106 | """Open a file. 107 | 108 | Args: 109 | file (str): File location 110 | 111 | Returns: 112 | dict: Data from the chosen file. 113 | """ 114 | path = os.path.dirname(os.path.realpath(__file__)) + "/data/" + file 115 | path = path.replace("/pyhiveapi/", "/apyhiveapi/") 116 | with open(path) as j: 117 | data = json.loads(j.read()) 118 | 119 | return data 120 | 121 | def addList(self, entityType: str, data: dict, **kwargs: dict): 122 | """Add entity to the list. 123 | 124 | Args: 125 | type (str): Type of entity 126 | data (dict): Information to create entity. 127 | 128 | Returns: 129 | dict: Entity. 130 | """ 131 | try: 132 | device = self.helper.getDeviceData(data) 133 | device_name = ( 134 | device["state"]["name"] 135 | if device["state"]["name"] != "Receiver" 136 | else "Heating" 137 | ) 138 | formatted_data = {} 139 | 140 | formatted_data = { 141 | "hiveID": data.get("id", ""), 142 | "hiveName": device_name, 143 | "hiveType": data.get("type", ""), 144 | "haType": entityType, 145 | "deviceData": device.get("props", data.get("props", {})), 146 | "parentDevice": data.get("parent", None), 147 | "isGroup": data.get("isGroup", False), 148 | "device_id": device["id"], 149 | "device_name": device_name, 150 | } 151 | 152 | if kwargs.get("haName", "FALSE")[0] == " ": 153 | kwargs["haName"] = device_name + kwargs["haName"] 154 | else: 155 | formatted_data["haName"] = device_name 156 | formatted_data.update(kwargs) 157 | self.deviceList[entityType].append(formatted_data) 158 | return formatted_data 159 | except KeyError as error: 160 | self.logger.error(error) 161 | return None 162 | 163 | async def updateInterval(self, new_interval: timedelta): 164 | """Update the scan interval. 165 | 166 | Args: 167 | new_interval (int): New interval for polling. 168 | """ 169 | if isinstance(new_interval, int): 170 | new_interval = timedelta(seconds=new_interval) 171 | 172 | interval = new_interval 173 | if interval < timedelta(seconds=15): 174 | interval = timedelta(seconds=15) 175 | self.config.scanInterval = interval 176 | 177 | async def useFile(self, username: str = None): 178 | """Update to check if file is being used. 179 | 180 | Args: 181 | username (str, optional): Looks for use@file.com. Defaults to None. 182 | """ 183 | using_file = True if username == "use@file.com" else False 184 | if using_file: 185 | self.config.file = True 186 | 187 | async def updateTokens(self, tokens: dict, update_expiry_time: bool = True): 188 | """Update session tokens. 189 | 190 | Args: 191 | tokens (dict): Tokens from API response. 192 | refresh_interval (Boolean): Should the refresh internval be updated 193 | 194 | Returns: 195 | dict: Parsed dictionary of tokens 196 | """ 197 | data = {} 198 | if "AuthenticationResult" in tokens: 199 | data = tokens.get("AuthenticationResult") 200 | self.tokens.tokenData.update({"token": data["IdToken"]}) 201 | if "RefreshToken" in data: 202 | self.tokens.tokenData.update({"refreshToken": data["RefreshToken"]}) 203 | self.tokens.tokenData.update({"accessToken": data["AccessToken"]}) 204 | if update_expiry_time: 205 | self.tokens.tokenCreated = datetime.now() 206 | elif "token" in tokens: 207 | data = tokens 208 | self.tokens.tokenData.update({"token": data["token"]}) 209 | self.tokens.tokenData.update({"refreshToken": data["refreshToken"]}) 210 | self.tokens.tokenData.update({"accessToken": data["accessToken"]}) 211 | 212 | if "ExpiresIn" in data: 213 | self.tokens.tokenExpiry = timedelta(seconds=data["ExpiresIn"]) 214 | 215 | return self.tokens 216 | 217 | async def login(self): 218 | """Login to hive account. 219 | 220 | Raises: 221 | HiveUnknownConfiguration: Login information is unknown. 222 | 223 | Returns: 224 | dict: result of the authentication request. 225 | """ 226 | result = None 227 | if not self.auth: 228 | raise HiveUnknownConfiguration 229 | 230 | try: 231 | result = await self.auth.login() 232 | except HiveInvalidUsername: 233 | print("invalid_username") 234 | except HiveInvalidPassword: 235 | print("invalid_password") 236 | except HiveApiError: 237 | print("no_internet_available") 238 | 239 | if "AuthenticationResult" in result: 240 | await self.updateTokens(result) 241 | return result 242 | 243 | async def sms2fa(self, code, session): 244 | """Login to hive account with 2 factor authentication. 245 | 246 | Raises: 247 | HiveUnknownConfiguration: Login information is unknown. 248 | 249 | Returns: 250 | dict: result of the authentication request. 251 | """ 252 | result = None 253 | if not self.auth: 254 | raise HiveUnknownConfiguration 255 | 256 | try: 257 | result = await self.auth.sms_2fa(code, session) 258 | except HiveInvalid2FACode: 259 | print("invalid_code") 260 | except HiveApiError: 261 | print("no_internet_available") 262 | 263 | if "AuthenticationResult" in result: 264 | await self.updateTokens(result) 265 | return result 266 | 267 | async def deviceLogin(self): 268 | """Login to hive account using device authentication. 269 | 270 | Raises: 271 | HiveUnknownConfiguration: Login information is unknown. 272 | HiveInvalidDeviceAuthentication: Device information is unknown. 273 | 274 | Returns: 275 | dict: result of the authentication request. 276 | """ 277 | result = None 278 | if not self.auth: 279 | raise HiveUnknownConfiguration 280 | 281 | try: 282 | result = await self.auth.device_login() 283 | except HiveInvalidDeviceAuthentication: 284 | raise HiveInvalidDeviceAuthentication 285 | 286 | if "AuthenticationResult" in result: 287 | await self.updateTokens(result) 288 | self.tokens.tokenExpiry = timedelta(seconds=0) 289 | return result 290 | 291 | async def hiveRefreshTokens(self): 292 | """Refresh Hive tokens. 293 | 294 | Returns: 295 | boolean: True/False if update was successful 296 | """ 297 | result = None 298 | 299 | if self.config.file: 300 | return None 301 | else: 302 | expiry_time = self.tokens.tokenCreated + self.tokens.tokenExpiry 303 | if datetime.now() >= expiry_time: 304 | result = await self.auth.refresh_token( 305 | self.tokens.tokenData["refreshToken"] 306 | ) 307 | 308 | if "AuthenticationResult" in result: 309 | await self.updateTokens(result) 310 | else: 311 | raise HiveFailedToRefreshTokens 312 | 313 | return result 314 | 315 | async def updateData(self, device: dict): 316 | """Get latest data for Hive nodes - rate limiting. 317 | 318 | Args: 319 | device (dict): Device requesting the update. 320 | 321 | Returns: 322 | boolean: True/False if update was successful 323 | """ 324 | updated = False 325 | ep = self.config.lastUpdate + self.config.scanInterval 326 | if datetime.now() >= ep and not self.updateLock.locked(): 327 | try: 328 | await self.updateLock.acquire() 329 | await self.getDevices(device["hiveID"]) 330 | if len(self.deviceList["camera"]) > 0: 331 | for camera in self.data.camera: 332 | await self.getCamera(self.devices[camera]) 333 | updated = True 334 | finally: 335 | self.updateLock.release() 336 | 337 | return updated 338 | 339 | async def getAlarm(self): 340 | """Get alarm data. 341 | 342 | Raises: 343 | HTTPException: HTTP error has occurred updating the devices. 344 | HiveApiError: An API error code has been returned. 345 | """ 346 | if self.config.file: 347 | api_resp_d = self.openFile("alarm.json") 348 | elif self.tokens is not None: 349 | api_resp_d = await self.api.getAlarm() 350 | if operator.contains(str(api_resp_d["original"]), "20") is False: 351 | raise HTTPException 352 | elif api_resp_d["parsed"] is None: 353 | raise HiveApiError 354 | 355 | self.data.alarm = api_resp_d["parsed"] 356 | 357 | async def getCamera(self, device): 358 | """Get camera data. 359 | 360 | Raises: 361 | HTTPException: HTTP error has occurred updating the devices. 362 | HiveApiError: An API error code has been returned. 363 | """ 364 | cameraImage = None 365 | cameraRecording = None 366 | hasCameraImage = False 367 | hasCameraRecording = False 368 | 369 | if self.config.file: 370 | cameraImage = self.openFile("camera.json") 371 | cameraRecording = self.openFile("camera.json") 372 | elif self.tokens is not None: 373 | cameraImage = await self.api.getCameraImage(device) 374 | hasCameraRecording = bool( 375 | cameraImage["parsed"]["events"][0]["hasRecording"] 376 | ) 377 | if hasCameraRecording: 378 | cameraRecording = await self.api.getCameraRecording( 379 | device, cameraImage["parsed"]["events"][0]["eventId"] 380 | ) 381 | 382 | if operator.contains(str(cameraImage["original"]), "20") is False: 383 | raise HTTPException 384 | elif cameraImage["parsed"] is None: 385 | raise HiveApiError 386 | else: 387 | raise NoApiToken 388 | 389 | hasCameraImage = bool(cameraImage["parsed"]["events"][0]) 390 | 391 | self.data.camera[device["id"]] = {} 392 | self.data.camera[device["id"]]["cameraImage"] = None 393 | self.data.camera[device["id"]]["cameraRecording"] = None 394 | 395 | if cameraImage is not None and hasCameraImage: 396 | self.data.camera[device["id"]] = {} 397 | self.data.camera[device["id"]]["cameraImage"] = cameraImage["parsed"][ 398 | "events" 399 | ][0] 400 | if cameraRecording is not None and hasCameraRecording: 401 | self.data.camera[device["id"]]["cameraRecording"] = cameraRecording[ 402 | "parsed" 403 | ] 404 | 405 | async def getDevices(self, n_id: str): 406 | """Get latest data for Hive nodes. 407 | 408 | Args: 409 | n_id (str): ID of the device requesting data. 410 | 411 | Raises: 412 | HTTPException: HTTP error has occurred updating the devices. 413 | HiveApiError: An API error code has been returned. 414 | 415 | Returns: 416 | boolean: True/False if update was successful. 417 | """ 418 | get_nodes_successful = False 419 | api_resp_d = None 420 | 421 | try: 422 | if self.config.file: 423 | api_resp_d = self.openFile("data.json") 424 | elif self.tokens is not None: 425 | await self.hiveRefreshTokens() 426 | api_resp_d = await self.api.getAll() 427 | if operator.contains(str(api_resp_d["original"]), "20") is False: 428 | raise HTTPException 429 | elif api_resp_d["parsed"] is None: 430 | raise HiveApiError 431 | 432 | api_resp_p = api_resp_d["parsed"] 433 | tmpProducts = {} 434 | tmpDevices = {} 435 | tmpActions = {} 436 | 437 | for hiveType in api_resp_p: 438 | if hiveType == "user": 439 | self.data.user = api_resp_p[hiveType] 440 | self.config.userID = api_resp_p[hiveType]["id"] 441 | if hiveType == "products": 442 | for aProduct in api_resp_p[hiveType]: 443 | tmpProducts.update({aProduct["id"]: aProduct}) 444 | if hiveType == "devices": 445 | for aDevice in api_resp_p[hiveType]: 446 | tmpDevices.update({aDevice["id"]: aDevice}) 447 | if aDevice["type"] == "siren": 448 | self.config.alarm = True 449 | # if aDevice["type"] == "hivecamera": 450 | # await self.getCamera(aDevice) 451 | if hiveType == "actions": 452 | for aAction in api_resp_p[hiveType]: 453 | tmpActions.update({aAction["id"]: aAction}) 454 | if hiveType == "homes": 455 | self.config.homeID = api_resp_p[hiveType]["homes"][0]["id"] 456 | 457 | if len(tmpProducts) > 0: 458 | self.data.products = copy.deepcopy(tmpProducts) 459 | if len(tmpDevices) > 0: 460 | self.data.devices = copy.deepcopy(tmpDevices) 461 | self.data.actions = copy.deepcopy(tmpActions) 462 | if self.config.alarm: 463 | await self.getAlarm() 464 | self.config.lastUpdate = datetime.now() 465 | get_nodes_successful = True 466 | except (OSError, RuntimeError, HiveApiError, ConnectionError, HTTPException): 467 | get_nodes_successful = False 468 | 469 | return get_nodes_successful 470 | 471 | async def startSession(self, config: dict = {}): 472 | """Setup the Hive platform. 473 | 474 | Args: 475 | config (dict, optional): Configuration for Home Assistant to use. Defaults to {}. 476 | 477 | Raises: 478 | HiveUnknownConfiguration: Unknown configuration identifed. 479 | HiveReauthRequired: Tokens have expired and reauthentication is required. 480 | 481 | Returns: 482 | list: List of devices 483 | """ 484 | await self.useFile(config.get("username", self.config.username)) 485 | await self.updateInterval( 486 | config.get("options", {}).get("scan_interval", self.config.scanInterval) 487 | ) 488 | 489 | if config != {}: 490 | if "tokens" in config and not self.config.file: 491 | await self.updateTokens(config["tokens"], False) 492 | 493 | if "device_data" in config and not self.config.file: 494 | self.auth.device_group_key = config["device_data"][0] 495 | self.auth.device_key = config["device_data"][1] 496 | self.auth.device_password = config["device_data"][2] 497 | 498 | if not self.config.file and "tokens" not in config: 499 | raise HiveUnknownConfiguration 500 | 501 | try: 502 | await self.getDevices("No_ID") 503 | except HTTPException: 504 | return HTTPException 505 | 506 | if self.data.devices == {} or self.data.products == {}: 507 | raise HiveReauthRequired 508 | 509 | return await self.createDevices() 510 | 511 | async def createDevices(self): 512 | """Create list of devices. 513 | 514 | Returns: 515 | list: List of devices 516 | """ 517 | self.deviceList["alarm_control_panel"] = [] 518 | self.deviceList["binary_sensor"] = [] 519 | self.deviceList["camera"] = [] 520 | self.deviceList["climate"] = [] 521 | self.deviceList["light"] = [] 522 | self.deviceList["sensor"] = [] 523 | self.deviceList["switch"] = [] 524 | self.deviceList["water_heater"] = [] 525 | 526 | hive_type = HIVE_TYPES["Heating"] + HIVE_TYPES["Switch"] + HIVE_TYPES["Light"] 527 | for aProduct in self.data.products: 528 | p = self.data.products[aProduct] 529 | if "error" in p: 530 | continue 531 | # Only consider single items or heating groups 532 | if ( 533 | p.get("isGroup", False) 534 | and self.data.products[aProduct]["type"] not in HIVE_TYPES["Heating"] 535 | ): 536 | continue 537 | product_list = PRODUCTS.get(self.data.products[aProduct]["type"], []) 538 | product_name = self.data.products[aProduct]["state"].get("name", "Unknown") 539 | for code in product_list: 540 | try: 541 | eval("self." + code) 542 | except (NameError, AttributeError) as e: 543 | self.logger.warning(f"Device {product_name} cannot be setup - {e}") 544 | pass 545 | 546 | if self.data.products[aProduct]["type"] in hive_type: 547 | self.config.mode.append(p["id"]) 548 | 549 | hive_type = HIVE_TYPES["Thermo"] + HIVE_TYPES["Sensor"] 550 | for aDevice in self.data["devices"]: 551 | d = self.data.devices[aDevice] 552 | device_list = DEVICES.get(self.data.devices[aDevice]["type"], []) 553 | for code in device_list: 554 | eval("self." + code) 555 | 556 | if self.data["devices"][aDevice]["type"] in hive_type: 557 | self.config.battery.append(d["id"]) 558 | 559 | if "action" in HIVE_TYPES["Switch"]: 560 | for action in self.data["actions"]: 561 | a = self.data["actions"][action] # noqa: F841 562 | eval("self." + ACTIONS) 563 | 564 | return self.deviceList 565 | 566 | @staticmethod 567 | def epochTime(date_time: any, pattern: str, action: str): 568 | """date/time conversion to epoch. 569 | 570 | Args: 571 | date_time (any): epoch time or date and time to use. 572 | pattern (str): Pattern for converting to epoch. 573 | action (str): Convert from/to. 574 | 575 | Returns: 576 | any: Converted time. 577 | """ 578 | if action == "to_epoch": 579 | pattern = "%d.%m.%Y %H:%M:%S" 580 | epochtime = int(time.mktime(time.strptime(str(date_time), pattern))) 581 | return epochtime 582 | elif action == "from_epoch": 583 | date = datetime.fromtimestamp(int(date_time)).strftime(pattern) 584 | return date 585 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.6.2", "wheel", "unasync"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pre-commit 2 | boto3>=1.16.10 3 | botocore>=1.19.10 4 | requests 5 | aiohttp 6 | pyquery 7 | unasync 8 | loguru -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | tox -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = pyhive-integration 3 | description = A Python library to interface with the Hive API 4 | keywords = Hive API Library 5 | license = MIT 6 | author = KJonline24 7 | author_email = khole_47@icloud.com 8 | url = https://github.com/Pyhive/pyhiveapi 9 | project_urls = 10 | Source = https://github.com/Pyhive/Pyhiveapi 11 | Issue tracker = https://github.com/Pyhive/Pyhiveapi/issues 12 | classifiers = 13 | Development Status :: 5 - Production/Stable 14 | Intended Audience :: Developers 15 | License :: OSI Approved :: MIT License 16 | Programming Language :: Python :: 3.6 17 | Programming Language :: Python :: 3.7 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: Python :: 3.9 20 | 21 | [options] 22 | package_dir = 23 | = pyhiveapi 24 | packages = find: 25 | python_requires = >=3.6 26 | 27 | [options.packages.find] 28 | where = pyhiveapi 29 | 30 | [build-system] 31 | requires = ["setuptools>=40.6.2", "wheel", "unasync"] 32 | build-backend = "setuptools.build_meta" 33 | 34 | [bdist_wheel] 35 | universal = 1 36 | 37 | [settings] 38 | multi_line_output = 3 39 | include_trailing_comma = True 40 | known_third_party = aiohttp,boto3,botocore,pyquery,requests,setuptools,six,urllib3 41 | 42 | [tool.isort] 43 | profile = "black" 44 | 45 | 46 | [flake8] 47 | exclude = .git,lib,deps,build,test 48 | doctests = True 49 | # To work with Black 50 | # E501: line too long 51 | # D401: mood imperative 52 | # W503: line break before binary operator 53 | ignore = 54 | E501, 55 | D401, 56 | W503 57 | 58 | 59 | [mypy] 60 | python_version = 3.8 61 | show_error_codes = true 62 | ignore_errors = true 63 | follow_imports = silent 64 | ignore_missing_imports = true 65 | warn_incomplete_stub = true 66 | warn_redundant_casts = true 67 | warn_unused_configs = true 68 | 69 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup pyhiveapi package.""" 2 | # pylint: skip-file 3 | import os 4 | import re 5 | 6 | import unasync 7 | from setuptools import setup 8 | 9 | 10 | def requirements_from_file(filename="requirements.txt"): 11 | """Get requirements from file.""" 12 | with open(os.path.join(os.path.dirname(__file__), filename)) as r: 13 | reqs = r.read().strip().split("\n") 14 | # Return non empty lines and non comments 15 | return [r for r in reqs if re.match(r"^\w+", r)] 16 | 17 | 18 | setup( 19 | version="1.0.2", 20 | package_data={"data": ["*.json"]}, 21 | include_package_data=True, 22 | cmdclass={ 23 | "build_py": unasync.cmdclass_build_py( 24 | rules=[ 25 | unasync.Rule( 26 | "/apyhiveapi/", 27 | "/pyhiveapi/", 28 | additional_replacements={ 29 | "apyhiveapi": "pyhiveapi", 30 | "asyncio": "threading", 31 | }, 32 | ), 33 | unasync.Rule( 34 | "/apyhiveapi/api/", 35 | "/pyhiveapi/api/", 36 | additional_replacements={ 37 | "apyhiveapi": "pyhiveapi", 38 | }, 39 | ), 40 | ] 41 | ) 42 | }, 43 | install_requires=requirements_from_file(), 44 | extras_require={"dev": requirements_from_file("requirements_test.txt")}, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/API/async_auth.py: -------------------------------------------------------------------------------- 1 | """Test file.""" 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for pyhiveapi.""" 2 | -------------------------------------------------------------------------------- /tests/bandit.yaml: -------------------------------------------------------------------------------- 1 | # https://bandit.readthedocs.io/en/latest/config.html 2 | 3 | tests: 4 | - B103 5 | - B108 6 | - B306 7 | - B313 8 | - B314 9 | - B315 10 | - B316 11 | - B317 12 | - B318 13 | - B319 14 | - B320 15 | - B325 16 | - B601 17 | - B602 18 | - B604 19 | - B608 20 | - B609 21 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | """Mock services for tests.""" 2 | # pylint: skip-file 3 | 4 | 5 | class MockConfig: 6 | """Mock config for tests.""" 7 | 8 | 9 | class MockDevice: 10 | """Mock Device for tests.""" 11 | -------------------------------------------------------------------------------- /tests/test_hub.py: -------------------------------------------------------------------------------- 1 | """Test hub framework.""" 2 | 3 | 4 | def test_hub_smoke(): 5 | """Test for hub smoke.""" 6 | result = None 7 | 8 | assert result 9 | --------------------------------------------------------------------------------