Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/dev-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Manual workflow to build and publish dev Python packages from non-master branches.
# Publishes to TestPyPI for development/testing purposes.

name: Dev Publish

# yamllint disable-line rule:truthy
on:
workflow_dispatch:

permissions:
contents: read

jobs:
guard:
name: Block master branch
runs-on: ubuntu-latest
steps:
- name: Fail if running on master
if: github.ref == 'refs/heads/master'
run: |
echo "::error::Dev publish is not allowed on the master branch."
exit 1

dev-build:
name: Build dev package
runs-on: ubuntu-latest
needs: guard
steps:
- uses: actions/checkout@v5

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install build dependencies
run: python -m pip install build

- name: Build package
run: python -m build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dev-dists
path: dist/

dev-publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: dev-build
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/project/pyhive-integration

steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dev-dists
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def requirements_from_file(filename="requirements.txt"):


setup(
version="1.0.7",
version="1.0.8.dev10",
packages=["apyhiveapi", "apyhiveapi.api", "apyhiveapi.helper"],
package_dir={"apyhiveapi": "src"},
package_data={"data": ["*.json"]},
Expand Down
7 changes: 6 additions & 1 deletion src/action.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Hive Action Module."""

# pylint: skip-file
import logging

_LOGGER = logging.getLogger(__name__)


class HiveAction:
Expand Down Expand Up @@ -67,7 +70,7 @@ async def getState(self, device: dict):
data = self.session.data.actions[device["hiveID"]]
final = data["enabled"]
except KeyError as e:
await self.session.log.error(e)
_LOGGER.error(e)

return final

Expand All @@ -85,6 +88,7 @@ async def setStatusOn(self, device: dict):
final = False

if device["hiveID"] in self.session.data.actions:
_LOGGER.debug("Enabling action %s.", device["haName"])
await self.session.hiveRefreshTokens()
data = self.session.data.actions[device["hiveID"]]
data.update({"enabled": True})
Expand All @@ -110,6 +114,7 @@ async def setStatusOff(self, device: dict):
final = False

if device["hiveID"] in self.session.data.actions:
_LOGGER.debug("Disabling action %s.", device["haName"])
await self.session.hiveRefreshTokens()
data = self.session.data.actions[device["hiveID"]]
data.update({"enabled": False})
Expand Down
12 changes: 9 additions & 3 deletions src/alarm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Hive Alarm Module."""

# pylint: skip-file
import logging

_LOGGER = logging.getLogger(__name__)


class HiveHomeShield:
Expand All @@ -24,7 +27,7 @@ async def getMode(self):
data = self.session.data.alarm
state = data["mode"]
except KeyError as e:
await self.session.log.error(e)
_LOGGER.error(e)

return state

Expand All @@ -40,7 +43,7 @@ async def getState(self, device: dict):
data = self.session.data.devices[device["hiveID"]]
state = data["state"]["alarmActive"]
except KeyError as e:
await self.session.log.error(e)
_LOGGER.error(e)

return state

Expand All @@ -59,6 +62,7 @@ async def setMode(self, device: dict, mode: str):
device["hiveID"] in self.session.data.devices
and device["deviceData"]["online"]
):
_LOGGER.debug("Setting alarm mode to %s.", mode)
await self.session.hiveRefreshTokens()
resp = await self.session.api.setAlarm(mode=mode)
if resp["original"] == 200:
Expand Down Expand Up @@ -99,6 +103,7 @@ async def getAlarm(self, device: dict):

if device["deviceData"]["online"]:
self.session.helper.deviceRecovered(device["device_id"])
_LOGGER.debug("Updating alarm data for %s.", device["haName"])
data = self.session.data.devices[device["device_id"]]
dev_data = {
"hiveID": device["hiveID"],
Expand All @@ -123,7 +128,8 @@ async def getAlarm(self, device: dict):
self.session.devices.update({device["hiveID"]: dev_data})
return self.session.devices[device["hiveID"]]
else:
await self.session.log.errorCheck(
await self.session.helper.errorCheck(
device["device_id"], "ERROR", device["deviceData"]["online"]
)
device.setdefault("status", {"state": None, "mode": None})
return device
2 changes: 1 addition & 1 deletion src/api/hive_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, hiveSession=None, websession=None, token=None):
"actions": "/actions",
"nodes": "/nodes/{0}/{1}",
}
self.timeout = 10
self.timeout = 5
self.json_return = {
"original": "No response to Hive API request",
"parsed": "No response to Hive API request",
Expand Down
81 changes: 56 additions & 25 deletions src/api/hive_async_api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
"""Hive API Module."""

# pylint: skip-file
import asyncio
import json
import logging
import time
from typing import Optional

import requests
import urllib3
from aiohttp import ClientResponse, ClientSession, web_exceptions
from aiohttp import ClientResponse, ClientSession, ClientTimeout, web_exceptions
from pyquery import PyQuery

from ..helper.const import HTTP_UNAUTHORIZED
from ..helper.hive_exceptions import FileInUse, HiveApiError, NoApiToken
from ..helper.const import HTTP_FORBIDDEN, HTTP_UNAUTHORIZED
from ..helper.hive_exceptions import FileInUse, HiveApiError, HiveAuthError, NoApiToken

_LOGGER = logging.getLogger(__name__)

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

Expand Down Expand Up @@ -38,7 +43,7 @@ def __init__(self, hiveSession=None, websession: Optional[ClientSession] = None)
"long_lived": "https://api.prod.bgchprod.info/omnia/accessTokens",
"weather": "https://weather.prod.bgchprod.info/weather",
}
self.timeout = 10
self.timeout = 5
self.json_return = {
"original": "No response to Hive API request",
"parsed": "No response to Hive API request",
Expand All @@ -50,46 +55,64 @@ async def request(
self, method: str, url: str, camera: bool = False, **kwargs
) -> ClientResponse:
"""Make a request."""
_LOGGER.debug("API %s request to %s", method.upper(), url)
data = kwargs.get("data", None)

headers = {
"content-type": "application/json",
"Accept": "*/*",
"User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
}
try:
if camera:
headers = {
"content-type": "application/json",
"Accept": "*/*",
"Authorization": f"Bearer {self.session.tokens.tokenData['token']}",
"x-jwt-token": self.session.tokens.tokenData["token"],
"User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
}
headers["Authorization"] = (
f"Bearer {self.session.tokens.tokenData['token']}"
)
headers["x-jwt-token"] = self.session.tokens.tokenData["token"]
else:
headers = {
"content-type": "application/json",
"Accept": "*/*",
"Authorization": self.session.tokens.tokenData["token"],
"User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
}
headers["Authorization"] = self.session.tokens.tokenData["token"]
except KeyError:
if "sso" in url:
pass
else:
raise NoApiToken

auth_token = headers.get("Authorization", "")
_LOGGER.debug(
"Using token (len=%d, tail=…%s)",
len(auth_token),
auth_token[-4:] if len(auth_token) >= 4 else auth_token,
)

timeout = ClientTimeout(total=self.timeout)
req_start = time.monotonic()
async with self.websession.request(
method, url, headers=headers, data=data
method, url, headers=headers, data=data, timeout=timeout
) as resp:
await resp.text()
resp_body = await resp.text()
req_duration = time.monotonic() - req_start
_LOGGER.debug(
"API %s %s completed in %.2fs — HTTP %s",
method.upper(),
url,
req_duration,
resp.status,
)
if str(resp.status).startswith("20"):
return resp

if resp.status == HTTP_UNAUTHORIZED:
self.session.logger.error(
f"Hive token has expired when calling {url} - "
f"HTTP status is - {resp.status}"
if resp.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
_LOGGER.error(
f"Hive token rejected calling {url} - "
f"HTTP {resp.status} — response: {resp_body[:200]}"
)
raise HiveAuthError(
f"Token expired or forbidden calling {url} — HTTP {resp.status}"
)
elif url is not None and resp.status is not None:
self.session.logger.error(
_LOGGER.error(
f"Something has gone wrong calling {url} - "
f"HTTP status is - {resp.status}"
f"HTTP status is - {resp.status} — response: {resp_body[:200]}"
)

raise HiveApiError
Expand Down Expand Up @@ -146,10 +169,14 @@ async def getAll(self):
"""Build and query all endpoint."""
json_return = {}
url = self.urls["all"]
_LOGGER.debug("Fetching all nodes from Hive API.")
try:
resp = await self.request("get", url)
json_return.update({"original": resp.status})
json_return.update({"parsed": await resp.json(content_type=None)})
except asyncio.TimeoutError:
_LOGGER.warning("Hive API request timed out fetching all nodes.")
raise
except (OSError, RuntimeError, ZeroDivisionError):
await self.error()

Expand Down Expand Up @@ -276,6 +303,7 @@ async def getWeather(self, weather_url):

async def setState(self, n_type, n_id, **kwargs):
"""Set the state of a Device."""
_LOGGER.debug("Setting state for %s/%s: %s", n_type, n_id, kwargs)
json_return = {}
jsc = (
"{"
Expand All @@ -301,6 +329,7 @@ async def setState(self, n_type, n_id, **kwargs):

async def setAlarm(self, **kwargs):
"""Set the state of the alarm."""
_LOGGER.debug("Setting alarm state: %s", kwargs)
json_return = {}
jsc = (
"{"
Expand All @@ -326,6 +355,7 @@ async def setAlarm(self, **kwargs):

async def setAction(self, n_id, data):
"""Set the state of a Action."""
_LOGGER.debug("Setting action %s", n_id)
jsc = data
url = self.urls["actions"] + "/" + n_id
try:
Expand All @@ -341,6 +371,7 @@ async def setAction(self, n_id, data):

async def error(self):
"""An error has occurred interacting with the Hive API."""
_LOGGER.error("HTTP error occurred during Hive API interaction.")
raise web_exceptions.HTTPError

async def isFileBeingUsed(self):
Expand Down
Loading
Loading