Skip to content

Commit

Permalink
Merge pull request #32 from BottlecapDave/develop
Browse files Browse the repository at this point in the history
New release
  • Loading branch information
BottlecapDave authored Jan 20, 2024
2 parents 2d7dd04 + 73f98b8 commit 2f69906
Show file tree
Hide file tree
Showing 16 changed files with 226 additions and 187 deletions.
4 changes: 0 additions & 4 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
blank_issues_enabled: false
contact_links:
- name: Integration Support
url: https://github.com/BottlecapDave/HomeAssistant-OctopusEnergy/discussions
about: Please ask and answer questions here.
28 changes: 0 additions & 28 deletions .github/workflows/deploy_docs.yml

This file was deleted.

47 changes: 47 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Docs
on:
workflow_dispatch:
push:
paths:
- 'mkdocs.yml'
- '_docs/**'
jobs:
build_docs:
if: ${{ (github.repository_owner == 'BottlecapDave' && (github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main')) == false }}
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v3
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mkdocs-material
- run: mkdocs build --strict

deploy_docs:
if: ${{ github.repository_owner == 'BottlecapDave' && (github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main') }}
name: Deploy docs
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v3
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --strict --force
2 changes: 1 addition & 1 deletion .github/workflows/issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"

jobs:
close-issues:
if: ${{ github.repository_owner == 'BottlecapDave' }}
runs-on: ubuntu-latest
permissions:
issues: write
Expand Down
41 changes: 32 additions & 9 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
name: Main
on:
schedule:
- cron: '0 6 * * *'
- cron: '0 1 * * *'
push:
branches:
- develop
- main
paths-ignore:
- 'mkdocs.yml'
- '_docs/**'
pull_request:
workflow_dispatch:
paths-ignore:
- 'mkdocs.yml'
- '_docs/**'
jobs:
validate:
if: ${{ github.event_name != 'schedule' || github.repository_owner == 'BottlecapDave' }}
name: Validate
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
Expand All @@ -18,8 +25,9 @@ jobs:
uses: "hacs/action@main"
with:
category: "integration"
test:
name: Test
unit_tests:
if: ${{ github.event_name != 'schedule' || github.repository_owner == 'BottlecapDave' }}
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -31,18 +39,33 @@ jobs:
- name: Install Python modules
run: |
pip install -r requirements.test.txt
- name: Unit tests suite
- name: Run unit tests
run: |
python -m pytest tests/unit
- name: Integration tests suite
integration_tests:
if: ${{ github.event_name != 'schedule' || github.repository_owner == 'BottlecapDave' }}
name: Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: asdf_install
uses: asdf-vm/actions/install@v3
- name: Install Python modules
run: |
pip install -r requirements.test.txt
- name: Run integration tests
run: |
python -m pytest tests/integration
release:
name: Release
if: github.ref == 'refs/heads/main'
needs:
if: ${{ github.repository_owner == 'BottlecapDave' && github.ref == 'refs/heads/main' }}
needs:
- validate
- test
- unit_tests
- integration_tests
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ To install, place the contents of `custom_components` into the `<config director

## How to setup

Please follow the [setup guide](https://bottlecapdave.github.io/HomeAssistant-FirstBus/setup/account) to setup the integration. This guide details the configuration, along with the sensors that will be available to you.
Please follow the [setup guide](https://bottlecapdave.github.io/HomeAssistant-FirstBus/setup) to setup the integration. This guide details the configuration, along with the sensors that will be available to you.

## Docs

Expand All @@ -48,4 +48,4 @@ Before raising anything, please read through the [faq](https://bottlecapdave.git

## Sponsorship

If you are enjoying the integration, why not if possible, make a one off or monthly [GitHub sponsorship](https://github.com/sponsors/bottlecapdave).
If you are enjoying the integration, why not if possible, make a one off or monthly [GitHub sponsorship](https://github.com/sponsors/bottlecapdave).
28 changes: 20 additions & 8 deletions _docs/entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@ The sensor will pull the latest times for the stop every **5 minutes**. This is

The following attributes are available in addition

| Attribute | Notes |
|-----------|-------|
| `ServiceNumber` | The name/number of the next bus |
| `Destination` | The destination of the next bus |
| `Due` | The timestamp of when the next bus is due, in ISO format |
| `IsFG` | Determines if the bus is a First bus (`Y`) or not (`N`) |
| `IsLive` | Determines if the bus is being tracked (`Y`) or is from the timetable (`N`) |
| `stop` | The ATCO code of the bus stop that is being tracked |
| Attribute | Type | Notes |
|-----------|------|-------|
| `service_ref` | `string` | The internal reference for the service |
| `service_number` | `string` | The name/number of the next bus |
| `destination` | `string` | The destination of the next bus |
| `due` | `datetime` | The timestamp of when the next bus is due |
| `is_live` | `boolean` | Determines if the bus is being tracked (`true`) or is from the timetable (`false`) |
| `stop` | `string` | The ATCO code of the bus stop that is being tracked |
| `buses` | `list` | The collection of known upcoming buses for the stop |
| `data_last_updated` | `datetime` | The timestamp when the underlying data was last updated |

For each bus in `buses`, you can find the following attributes

| Attribute | Type | Notes |
|-----------|------|-------|
| `service_ref` | `string` | The internal reference for the service |
| `service_number` | `string` | The name/number of the next bus |
| `destination` | `string` | The destination of the next bus |
| `due` | `datetime` | The timestamp of when the next bus is due |
| `is_live` | `boolean` | Determines if the bus is being tracked (`true`) or is from the timetable (`false`) |
8 changes: 7 additions & 1 deletion custom_components/first_bus/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ async def async_get_bus_times(self, stop):
data = await response.json(content_type=None)

if ("times" in data):
return data["times"]
return list(map(lambda time: {
"due": time["Due"],
"service_ref": time["ServiceRef"],
"service_number": time["ServiceNumber"],
"destination": time["Destination"],
"is_live": True if time["IsLive"] == 'Y' or time["IsLive"] == 'y' else False
}, data["times"]))

return []
2 changes: 2 additions & 0 deletions custom_components/first_bus/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

DOMAIN = "first_bus"

MINUTES_BETWEEN_UPDATES = 5

CONFIG_NAME = "Name"
CONFIG_STOP = "Stop"
CONFIG_BUSES = "Buses"
Expand Down
2 changes: 1 addition & 1 deletion custom_components/first_bus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/BottlecapDave/HomeAssistant-FirstBus/",
"documentation": "https://bottlecapdave.github.io/HomeAssistant-FirstBus",
"homekit": {},
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/BottlecapDave/HomeAssistant-FirstBus/issues",
Expand Down
19 changes: 13 additions & 6 deletions custom_components/first_bus/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from .const import (
CONFIG_NAME,
CONFIG_STOP,
CONFIG_BUSES
CONFIG_BUSES,
MINUTES_BETWEEN_UPDATES
)

from .api_client import (FirstBusApiClient)
Expand Down Expand Up @@ -41,7 +42,8 @@ def __init__(self, data):
self._buses = []
self._attributes = {}
self._state = None
self._minsSinceLastUpdate = 0
self._mins_since_last_update = 0
self._data_last_updated = None

@property
def unique_id(self):
Expand Down Expand Up @@ -74,14 +76,16 @@ def state(self):

async def async_update(self):
"""Retrieve the next bus"""
self._minsSinceLastUpdate = self._minsSinceLastUpdate - 1
self._mins_since_last_update = self._mins_since_last_update - 1

# We only want to update every 5 minutes so we don't hammer the service
if self._minsSinceLastUpdate <= 0:
data_last_updated = None
if self._mins_since_last_update <= 0:
bus_times = await self._client.async_get_bus_times(self._data[CONFIG_STOP])
buses = get_buses(bus_times, now())
self._buses = buses
self._minsSinceLastUpdate = 5
self._mins_since_last_update = MINUTES_BETWEEN_UPDATES
self._data_last_updated = now()

next_bus = get_next_bus(self._buses, self._data[CONFIG_BUSES], now())
self._attributes = copy.copy(next_bus)
Expand All @@ -90,8 +94,11 @@ async def async_update(self):

self._attributes["stop"] = self._data[CONFIG_STOP]
self._attributes["buses"] = self._buses

if self._data_last_updated is not None:
self._attributes["data_last_updated"] = self._data_last_updated

if next_bus is not None:
self._state = next_bus["Due"]
self._state = next_bus["due"]
else:
self._state = None
28 changes: 14 additions & 14 deletions custom_components/first_bus/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,36 @@ def get_buses(bus_times: list, current_timestamp: datetime):
_LOGGER.debug(f'buses: {bus_times}')

for bus_time in bus_times:
matches = re.search(REGEX_TIME, bus_time["Due"])
matches = re.search(REGEX_TIME, bus_time["due"])
if (matches is not None):
bus_time["Due"] = parse_datetime(current_timestamp.strftime(f"%Y-%m-%dT{bus_time['Due']}{current_timestamp.strftime('%z')}"))
bus_time["due"] = parse_datetime(current_timestamp.strftime(f"%Y-%m-%dT{bus_time['due']}{current_timestamp.strftime('%z')}"))
else:
matches = re.search(REGEX_TIME_MINS, bus_time["Due"])
matches = re.search(REGEX_TIME_MINS, bus_time["due"])
if (matches is None):
if bus_time["Due"] == "Due now":
bus_time["Due"] = current_timestamp.replace(second=0, microsecond=0)
if bus_time["due"] == "Due now":
bus_time["due"] = current_timestamp.replace(second=0, microsecond=0)
else:
raise Exception(f'Unable to extract due time: {bus_time["Due"]}')
raise Exception(f'Unable to extract due time: {bus_time["due"]}')
else:
bus_time["Due"] = current_timestamp.replace(second=0, microsecond=0) + timedelta(minutes=int(matches[1]))
bus_time["due"] = current_timestamp.replace(second=0, microsecond=0) + timedelta(minutes=int(matches[1]))

if (bus_time["Due"] < current_timestamp.replace(second=0, microsecond=0)):
_LOGGER.debug(f'Moving due timestamp to next day: Due: {bus_time["Due"]}; Current Timestamp: {current_timestamp}')
bus_time["Due"] = bus_time["Due"] + timedelta(days=1)
if (bus_time["due"] < current_timestamp.replace(second=0, microsecond=0)):
_LOGGER.debug(f'Moving due timestamp to next day: Due: {bus_time["due"]}; Current Timestamp: {current_timestamp}')
bus_time["due"] = bus_time["due"] + timedelta(days=1)

return bus_times

def get_next_bus(bus_times, target_buses, current_timestamp):
def get_next_bus(bus_times: list, target_buses: list[str], current_timestamp: datetime):
next_bus = None
for bus_time in bus_times:
if (target_buses is None or len(target_buses) == 0 or bus_time["ServiceNumber"] in target_buses):
if (target_buses is None or len(target_buses) == 0 or bus_time["service_number"] in target_buses):

if bus_time["Due"] >= current_timestamp.replace(second=0, microsecond=0) and (next_bus is None or next_bus["Due"] > bus_time["Due"]):
if bus_time["due"] >= current_timestamp.replace(second=0, microsecond=0) and (next_bus is None or next_bus["due"] > bus_time["due"]):
next_bus = bus_time

return next_bus

def calculate_minutes_remaining(target_timestamp, current_timestamp):
def calculate_minutes_remaining(target_timestamp: datetime, current_timestamp: datetime):
if target_timestamp is not None and current_timestamp is not None:
if (target_timestamp >= current_timestamp):
return (target_timestamp - current_timestamp).seconds // 60
Expand Down
19 changes: 8 additions & 11 deletions tests/integration/api_client/test_get_bus_times.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,17 @@ async def test_when_get_buses_is_called_then_next_bus_is_returned():
for bus in buses:
assert bus is not None

assert "Due" in bus
assert bus["Due"] is not None
assert "due" in bus
assert bus["due"] is not None

assert "ServiceNumber" in bus
assert bus["ServiceNumber"] is not None
assert "service_number" in bus
assert bus["service_number"] is not None

assert "Destination" in bus
bus["Destination"] is not None
assert "destination" in bus
bus["destination"] is not None

assert "IsFG" in bus
assert bus["IsFG"] == "Y" or bus["IsFG"] == "N"

assert "IsLive" in bus
assert bus["IsLive"] == "Y" or bus["IsFG"] == "N"
assert "is_live" in bus
assert bus["is_live"] == True or bus["is_live"] == False

passes += 1
except:
Expand Down
Loading

0 comments on commit 2f69906

Please sign in to comment.