Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions .github/workflows/dev-release-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Open dev -> master release PR (with version bump)

on:
push:
branches: [dev]

permissions:
contents: write
pull-requests: write

jobs:
release-pr:
if: "!contains(github.event.head_commit.message, 'chore: bump version')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: Check for existing dev -> master PR
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
existing=$(gh pr list --base master --head dev --state open --json number --jq '.[0].number')
echo "existing=$existing" >> "$GITHUB_OUTPUT"
if [ -n "$existing" ]; then
echo "PR #$existing already open; skipping version bump."
fi

- name: Bump patch version in setup.py
if: steps.check.outputs.existing == ''
run: |
python - <<'PY'
import re, pathlib
p = pathlib.Path("setup.py")
s = p.read_text()
m = re.search(r'version="(\d+)\.(\d+)\.(\d+)"', s)
if not m:
raise SystemExit("version not found")
maj, mnr, pat = map(int, m.groups())
new = f'version="{maj}.{mnr}.{pat+1}"'
p.write_text(s[:m.start()] + new + s[m.end():])
print("Bumped to", new)
PY

- name: Commit bump
if: steps.check.outputs.existing == ''
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add setup.py
if ! git diff --cached --quiet; then
git commit -m "chore: bump version [skip ci]"
git push
fi

- name: Create dev -> master PR
if: steps.check.outputs.existing == ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create \
--base master \
--head dev \
--title "Release: dev -> master" \
--body "Automated release PR. Contains merged feature PRs and a single version bump."
17 changes: 17 additions & 0 deletions .github/workflows/guard-master.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Guard master branch

on:
pull_request:
branches: [master]

jobs:
source-branch-is-dev:
runs-on: ubuntu-latest
steps:
- name: Ensure PR source is dev
run: |
if [ "${{ github.head_ref }}" != "dev" ]; then
echo "::error::PRs into master must come from 'dev' (got '${{ github.head_ref }}')."
exit 1
fi
echo "Source branch is dev. OK."
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,38 @@ custom_tests/*

# virtual environment folder
.venv/


# IDE specific files
.vscode/
.idea/
*.swp
*.swo
*~

# OS specific files
.DS_Store


# Python cache files
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info/
dist/
build/
*.manifest
*.spec

# Virtual Environment
venv/
env/
ENV/
.venv


# Claude superpowers
superpowers/
1 change: 1 addition & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
tox
pylint
pytest
pytest-asyncio
pbr
70 changes: 11 additions & 59 deletions src/api/hive_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class HiveApi:

def __init__(self, hiveSession=None, websession=None, token=None):
"""Hive API initialisation."""
self.cameraBaseUrl = "prod.hcam.bgchtest.info"
self.urls = {
"properties": "https://sso.hivehome.com/",
"login": "https://beekeeper.hivehome.com/1.0/cognito/login",
Expand All @@ -29,8 +28,6 @@ def __init__(self, hiveSession=None, websession=None, token=None):
"holiday_mode": "/holiday-mode",
"all": "/nodes/all?products=true&devices=true&actions=true",
"alarm": "/security-lite?homeId=",
"cameraImages": f"https://event-history-service.{self.cameraBaseUrl}/v1/events/cameras?latest=true&cameraId={{0}}",
"cameraRecordings": f"https://event-history-service.{self.cameraBaseUrl}/v1/playlist/cameras/{{0}}/events/{{1}}.m3u8",
"devices": "/devices",
"products": "/products",
"actions": "/actions",
Expand All @@ -44,40 +41,24 @@ def __init__(self, hiveSession=None, websession=None, token=None):
self.session = hiveSession
self.token = token

def request(self, type, url, jsc=None, camera=False):
def request(self, type, url, jsc=None):
"""Make API request."""
_LOGGER.debug("request - Making %s request to: %s", type, url)
if jsc:
_LOGGER.debug("request - Request payload: %s", jsc)

if self.session is not None:
if camera:
self.headers = {
"content-type": "application/json",
"Accept": "*/*",
"Authorization": f"Bearer {self.session.tokens.tokenData['token']}",
"x-jwt-token": self.session.tokens.tokenData["token"],
}
else:
self.headers = {
"content-type": "application/json",
"Accept": "*/*",
"authorization": self.session.tokens.tokenData["token"],
}
self.headers = {
"content-type": "application/json",
"Accept": "*/*",
"authorization": self.session.tokens.tokenData["token"],
}
else:
if camera:
self.headers = {
"content-type": "application/json",
"Accept": "*/*",
"Authorization": f"Bearer {self.token}",
"x-jwt-token": self.token,
}
else:
self.headers = {
"content-type": "application/json",
"Accept": "*/*",
"authorization": self.token,
}
self.headers = {
"content-type": "application/json",
"Accept": "*/*",
"authorization": self.token,
}

_LOGGER.debug(
"request - Request headers: %s",
Expand Down Expand Up @@ -119,7 +100,6 @@ def refreshTokens(self, tokens={}):
)
self.session.updateTokens(data)
self.urls.update({"base": data["platform"]["endpoint"]})
self.urls.update({"camera": data["platform"]["cameraPlatform"]})
self.json_return.update({"original": info.status_code})
self.json_return.update({"parsed": info.json()})
except (OSError, RuntimeError, ZeroDivisionError, json.JSONDecodeError) as e:
Expand Down Expand Up @@ -200,34 +180,6 @@ def getAlarm(self, homeID=None):

return self.json_return

def getCameraImage(self, device=None, accessToken=None):
"""Build and query camera endpoint."""
json_return = {}
url = self.urls["cameraImages"].format(device["props"]["hardwareIdentifier"])
try:
info = self.request("GET", url, camera=True)
json_return.update({"original": info.status_code})
json_return.update({"parsed": info.json()})
except (OSError, RuntimeError, ZeroDivisionError):
self.error()

return json_return

def getCameraRecording(self, device=None, eventId=None):
"""Build and query camera endpoint."""
json_return = {}
url = self.urls["cameraRecordings"].format(
device["props"]["hardwareIdentifier"], eventId
)
try:
info = self.request("GET", url, camera=True)
json_return.update({"original": info.status_code})
json_return.update({"parsed": info.text.split("\n")[3]})
except (OSError, RuntimeError, ZeroDivisionError):
self.error()

return json_return

def getDevices(self):
"""Call the get devices endpoint."""
url = self.urls["base"] + self.urls["devices"]
Expand Down
45 changes: 2 additions & 43 deletions src/api/hive_async_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@ class HiveApiAsync:
def __init__(self, hiveSession=None, websession: Optional[ClientSession] = None):
"""Hive API initialisation."""
self.baseUrl = "https://beekeeper.hivehome.com/1.0"
self.cameraBaseUrl = "prod.hcam.bgchtest.info"
self.urls = {
"properties": "https://sso.hivehome.com/",
"login": f"{self.baseUrl}/cognito/login",
"refresh": f"{self.baseUrl}/cognito/refresh-token",
"holiday_mode": f"{self.baseUrl}/holiday-mode",
"all": f"{self.baseUrl}/nodes/all?products=true&devices=true&actions=true",
"alarm": f"{self.baseUrl}/security-lite?homeId=",
"cameraImages": f"https://event-history-service.{self.cameraBaseUrl}/v1/events/cameras?latest=true&cameraId={{0}}",
"cameraRecordings": f"https://event-history-service.{self.cameraBaseUrl}/v1/playlist/cameras/{{0}}/events/{{1}}.m3u8",
"devices": f"{self.baseUrl}/devices",
"products": f"{self.baseUrl}/products",
"actions": f"{self.baseUrl}/actions",
Expand All @@ -51,9 +48,7 @@ def __init__(self, hiveSession=None, websession: Optional[ClientSession] = None)
self.session = hiveSession
self.websession = ClientSession() if websession is None else websession

async def request(
self, method: str, url: str, camera: bool = False, **kwargs
) -> ClientResponse:
async def request(self, method: str, url: str, **kwargs) -> ClientResponse:
"""Make a request."""
_LOGGER.debug("API %s request to %s", method.upper(), url)
data = kwargs.get("data", None)
Expand All @@ -64,13 +59,7 @@ async def request(
"User-Agent": "Hive/12.04.0 iOS/18.3.1 Apple",
}
try:
if camera:
headers["Authorization"] = (
f"Bearer {self.session.tokens.tokenData['token']}"
)
headers["x-jwt-token"] = self.session.tokens.tokenData["token"]
else:
headers["Authorization"] = self.session.tokens.tokenData["token"]
headers["Authorization"] = self.session.tokens.tokenData["token"]
except KeyError:
if "sso" in url:
pass
Expand Down Expand Up @@ -158,7 +147,6 @@ async def refreshTokens(self):
if "token" in info:
await self.session.updateTokens(info)
self.baseUrl = info["platform"]["endpoint"]
self.cameraBaseUrl = info["platform"]["cameraPlatform"]
return True
except (ConnectionError, OSError, RuntimeError, ZeroDivisionError):
await self.error()
Expand Down Expand Up @@ -194,35 +182,6 @@ async def getAlarm(self):

return json_return

async def getCameraImage(self, device):
"""Build and query alarm endpoint."""
json_return = {}
url = self.urls["cameraImages"].format(device["props"]["hardwareIdentifier"])
try:
resp = await self.request("get", url, True)
json_return.update({"original": resp.status})
json_return.update({"parsed": await resp.json(content_type=None)})
except (OSError, RuntimeError, ZeroDivisionError):
await self.error()

return json_return

async def getCameraRecording(self, device, eventId):
"""Build and query alarm endpoint."""
json_return = {}
url = self.urls["cameraRecordings"].format(
device["props"]["hardwareIdentifier"], eventId
)
try:
resp = await self.request("get", url, True)
recUrl = await resp.text()
json_return.update({"original": resp.status})
json_return.update({"parsed": recUrl.split("\n")[3]})
except (OSError, RuntimeError, ZeroDivisionError):
await self.error()

return json_return

async def getDevices(self):
"""Call the get devices endpoint."""
json_return = {}
Expand Down
Loading
Loading