diff --git a/.build/createGithubRelease.ts b/.build/createGithubRelease.ts new file mode 100644 index 0000000..90cd56d --- /dev/null +++ b/.build/createGithubRelease.ts @@ -0,0 +1,53 @@ +const bodySuffix = "---\nEnjoying the integration? Why not make a one time or monthly [GitHub sponsorship](https://github.com/sponsors/bottlecapdave)?" + +async function createGithubRelease(githubToken: string, githubOwnerRepo: string, tag: string, notes: string) { + if (!githubToken) { + throw new Error('Github token not specified'); + } + + if (!githubOwnerRepo) { + throw new Error('Github owner/repo not specified'); + } + + if (!tag) { + throw new Error('Tag not specified'); + } + + if (!notes) { + throw new Error('Notes not specified'); + } + + console.log(`Publishing ${tag} release to ${githubOwnerRepo}`); + + const body = JSON.stringify({ + tag_name: tag, + name: tag, + body: notes, + draft: false, + prerelease:false + }); + + const response = await fetch( + `https://api.github.com/repos/${githubOwnerRepo}/releases`, + { + method: 'POST', + headers: { + "Accept": "application/vnd.github+json", + "Authorization": `Bearer ${githubToken}`, + "X-GitHub-Api-Version": "2022-11-28" + }, + body + } + ); + + if (response.status >= 300) { + throw new Error(response.statusText); + } +} + +createGithubRelease( + process.env.GITHUB_TOKEN, + process.env.GITHUB_REPOSITORY, + process.argv[2], + `${process.argv[3]}\n${bodySuffix}` +).then(() => console.log('Success')); \ No newline at end of file diff --git a/.build/tsconfig.json b/.build/tsconfig.json new file mode 100644 index 0000000..88259a9 --- /dev/null +++ b/.build/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "module": "commonjs", + "noImplicitAny": true, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + + }, + "include": ["**/*"] +} \ No newline at end of file diff --git a/.build/update-manifest.js b/.build/update-manifest.js deleted file mode 100644 index a5161dd..0000000 --- a/.build/update-manifest.js +++ /dev/null @@ -1,14 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const fileName = path.join(__dirname, '../custom_components/first_bus/manifest.json'); -const file = require(fileName); - -file.version = process.argv[2]; - -fs.writeFile(fileName, JSON.stringify(file, null, 2), function writeJSON(err) { - if (err) { - return console.log(err); - } - - return `Updated version to ${file.version}`; -}); \ No newline at end of file diff --git a/.build/updateManifest.ts b/.build/updateManifest.ts new file mode 100644 index 0000000..af9734d --- /dev/null +++ b/.build/updateManifest.ts @@ -0,0 +1,16 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +const filePath = join(__dirname, '../custom_components/first_bus/manifest.json'); + +function updateManifest(version: string) { + + const buffer = readFileSync(filePath); + const content = JSON.parse(buffer.toString()); + content.version = version; + + writeFileSync(filePath, JSON.stringify(content, null, 2)); + console.log(`Updated version to '${version}'`); +} + +updateManifest(process.argv[2]); \ No newline at end of file diff --git a/.releaserc.json b/.releaserc.json index 5936a9b..61550df 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,5 +1,5 @@ { - "branches": ["main"], + "branches": ["main", "develop"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", @@ -9,10 +9,9 @@ "changelogFile": "CHANGELOG.md" } ], - "@semantic-release/github", [ "@semantic-release/exec", { - "prepareCmd" : "node .build/update-manifest ${nextRelease.version}" + "prepareCmd" : "ts-node .build/updateManifest.ts ${nextRelease.version}" } ], [ @@ -20,6 +19,11 @@ "assets": ["package.json", "CHANGELOG.md", "./custom_components/first_bus/manifest.json"], "message": "release: Released v${nextRelease.version} [skip ci]" } + ], + [ + "@semantic-release/exec", { + "publishCmd" : "ts-node .build/createGithubRelease.ts v${nextRelease.version} \"${nextRelease.notes}\"" + } ] ] } \ No newline at end of file diff --git a/README.md b/README.md index 5bdba19..a4d821a 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,11 @@ However, there have been reports of missing ATCO codes. Therefore alternatively, You can setup the integration as many times as you like, with each time tracking a different stop and potential buses from that stop. -Each instance will create a single sensor in the form `sensor.first_bus_<>_next_bus`. This will provide the number of minutes until one of the specified buses (or any if none were specified) reach the bus stop. The following attributes are available in addition +Each instance will create a single sensor in the form `sensor.first_bus_<>_next_bus`. This will provide the number of minutes until one of the specified buses (or any if no specific buses were specified) reach the bus stop. If there is no known next bus, then `none`/`unknown` will be returned. + +The sensor will pull the latest times for the stop every **5 minutes**. This is done so that the unofficial API isn't hammered and support is taken away, and I felt 5 minutes was long enough so that information wasn't too stale. This means that there is a chance that the time won't reflect the times on the app/website if they are updated within this 5 minute timeframe. + +The following attributes are available in addition | Attribute | Notes | |-----------|-------| @@ -70,4 +74,4 @@ Each instance will create a single sensor in the form `sensor.first_bus_< 0: + matches = re.search(REGEX_BUSES, new_config[CONFIG_BUSES]) + if (matches is None): + errors[CONFIG_BUSES] = "invalid_buses" + else: + new_config[CONFIG_BUSES] = new_config[CONFIG_BUSES].split(",") + else: + new_config[CONFIG_BUSES] = [] + + return (errors, new_config) \ No newline at end of file diff --git a/custom_components/first_bus/config_flow.py b/custom_components/first_bus/config_flow.py index c0f4647..f53bdc4 100644 --- a/custom_components/first_bus/config_flow.py +++ b/custom_components/first_bus/config_flow.py @@ -1,5 +1,4 @@ import voluptuous as vol -import re import logging from homeassistant.config_entries import (ConfigFlow, OptionsFlow) @@ -12,9 +11,10 @@ CONFIG_BUSES, DATA_SCHEMA_STOP, - REGEX_BUSES, ) +from .config import merge_config, validate_config + _LOGGER = logging.getLogger(__name__) class FirstBusConfigFlow(ConfigFlow, domain=DOMAIN): @@ -27,20 +27,13 @@ async def async_step_user(self, user_input): errors = {} if user_input is not None: - if CONFIG_BUSES in user_input and user_input[CONFIG_BUSES] is not None: - matches = re.search(REGEX_BUSES, user_input[CONFIG_BUSES]) - if (matches is None): - errors[CONFIG_BUSES] = "invalid_buses" - else: - user_input[CONFIG_BUSES] = user_input[CONFIG_BUSES].split(",") - else: - user_input[CONFIG_BUSES] = [] + (errors, config) = validate_config(user_input) # Setup our basic sensors if len(errors) < 1: return self.async_create_entry( - title=f"Bus Stop {user_input[CONFIG_NAME]}", - data=user_input + title=f"Bus Stop {config[CONFIG_NAME]}", + data=config ) return self.async_show_form( @@ -61,46 +54,43 @@ def __init__(self, entry) -> None: async def async_step_init(self, user_input): """Manage the options for the custom component.""" - config = dict(self._entry.data) - if self._entry.options is not None: - config.update(self._entry.options) + config = merge_config(self._entry.data, self._entry.options) return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Optional(CONFIG_BUSES, default=','.join(config[CONFIG_BUSES])): str, - }) + data_schema=self.add_suggested_values_to_schema( + vol.Schema({ + vol.Optional(CONFIG_BUSES): str, + }), + { + CONFIG_BUSES: ','.join(config[CONFIG_BUSES]) + } + ) ) async def async_step_user(self, user_input): """Manage the options for the custom component.""" errors = {} - config = dict(self._entry.data) - if self._entry.options is not None: - config.update(self._entry.options) - - if user_input is not None: - config.update(user_input) + + config = merge_config(self._entry.data, self._entry.options, user_input if user_input is not None else {}) _LOGGER.debug(f"Update config {config}") - if CONFIG_BUSES in config and config[CONFIG_BUSES] is not None and len(config[CONFIG_BUSES]) > 0: - matches = re.search(REGEX_BUSES, config[CONFIG_BUSES]) - if (matches is None): - errors[CONFIG_BUSES] = "invalid_buses" - else: - config[CONFIG_BUSES] = config[CONFIG_BUSES].split(",") - else: - config[CONFIG_BUSES] = [] + (errors, config) = validate_config(config) if len(errors) < 1: return self.async_create_entry(title="", data=config) return self.async_show_form( step_id="user", - data_schema=vol.Schema({ - vol.Optional(CONFIG_BUSES, default=','.join(config[CONFIG_BUSES])): str, - }), + data_schema=self.add_suggested_values_to_schema( + vol.Schema({ + vol.Optional(CONFIG_BUSES): str, + }), + { + CONFIG_BUSES: ','.join(config[CONFIG_BUSES]) + } + ), errors=errors ) \ No newline at end of file diff --git a/custom_components/first_bus/const.py b/custom_components/first_bus/const.py index 7d0d723..dd48fb4 100644 --- a/custom_components/first_bus/const.py +++ b/custom_components/first_bus/const.py @@ -6,7 +6,7 @@ CONFIG_STOP = "Stop" CONFIG_BUSES = "Buses" -REGEX_BUSES="^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$" +REGEX_BUSES="^[a-zA-Z0-9 ]+(,[a-zA-Z0-9 ]+)*$" REGEX_TIME="^[0-9]{2}:[0-9]{2}$" REGEX_TIME_MINS="([0-9]+) mins" diff --git a/custom_components/first_bus/sensor.py b/custom_components/first_bus/sensor.py index f21344a..28a1460 100644 --- a/custom_components/first_bus/sensor.py +++ b/custom_components/first_bus/sensor.py @@ -14,7 +14,7 @@ from .api_client import (FirstBusApiClient) from .utils import ( get_next_bus, - async_get_buses, + get_buses, calculate_minutes_remaining ) @@ -77,7 +77,8 @@ async def async_update(self): # We only want to update every 5 minutes so we don't hammer the service if self._minsSinceLastUpdate <= 0: - buses = await async_get_buses(self._client, self._data[CONFIG_STOP], now()) + 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 diff --git a/custom_components/first_bus/translations/en.json b/custom_components/first_bus/translations/en.json index af203fa..f17061c 100644 --- a/custom_components/first_bus/translations/en.json +++ b/custom_components/first_bus/translations/en.json @@ -6,14 +6,18 @@ "title": "First Bus Stop", "description": "Setup your bus stop information.", "data": { - "Name": "The friendly name for the stop", + "Name": "Name", "Stop": "Stop ATCO code", - "Buses": "The buses you want to filter to. If you want all buses, leave blank." + "Buses": "Buses" + }, + "data_description": { + "Stop": "Instructions to find your ATCO code can be found at https://github.com/BottlecapDave/HomeAssistant-FirstBus#atco-code", + "Buses": "You can filter to specific buses, comma separated, or leave blank to see all buses" } } }, "error": { - "invalid_buses": "Buses must be alphanumeric separated by commas" + "invalid_buses": "Buses must be alphanumeric and spaces separated by commas" }, "abort": { } @@ -24,13 +28,15 @@ "title": "First Bus Stop", "description": "Update your bus stop information.", "data": { - "Name": "The friendly name for the stop", - "Buses": "The buses you want to filter to. If you want all buses, leave blank." + "Buses": "Buses" + }, + "data_description": { + "Buses": "You can filter to specific buses, comma separated, or leave blank to see all buses" } } }, "error": { - "invalid_buses": "Buses must be alphanumeric separated by commas" + "invalid_buses": "Buses must be alphanumeric and spaces separated by commas" }, "abort": { } diff --git a/custom_components/first_bus/utils.py b/custom_components/first_bus/utils.py index 08d2956..82040fa 100644 --- a/custom_components/first_bus/utils.py +++ b/custom_components/first_bus/utils.py @@ -1,19 +1,16 @@ import logging import re +from datetime import (datetime, timedelta) +from homeassistant.util.dt import (parse_datetime) from .const import ( REGEX_TIME, REGEX_TIME_MINS, ) -from datetime import (timedelta) -from homeassistant.util.dt import (parse_datetime) -from .api_client import FirstBusApiClient - _LOGGER = logging.getLogger(__name__) -async def async_get_buses(api_client: FirstBusApiClient, stop, current_timestamp): - bus_times = await api_client.async_get_buses(stop) +def get_buses(bus_times: list, current_timestamp: datetime): _LOGGER.debug(f'buses: {bus_times}') for bus_time in bus_times: diff --git a/package-lock.json b/package-lock.json index c84bc53..59dcf0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "home-assistant-first-bus", "version": "1.0.0", "license": "ISC", + "dependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, "devDependencies": { "@semantic-release/changelog": "^6.0.1", "@semantic-release/exec": "^6.0.2", @@ -161,6 +165,20 @@ "node": ">=8" } }, + "node_modules/@commitlint/load/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "optional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@commitlint/resolve-extends": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-13.2.0.tgz", @@ -266,6 +284,17 @@ "node": ">=8" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", @@ -285,6 +314,55 @@ "cosmiconfig": ">=6" } }, + "node_modules/@endemolshinegroup/cosmiconfig-typescript-loader/node_modules/ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "optional": true, + "dependencies": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -653,12 +731,41 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "node_modules/@types/node": { + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "peer": true, + "dependencies": { + "undici-types": "~5.25.1" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -677,6 +784,25 @@ "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -759,9 +885,7 @@ "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, "node_modules/argv-formatter": { "version": "1.0.0", @@ -1251,9 +1375,7 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -1452,8 +1574,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "optional": true, "engines": { "node": ">=0.3.1" } @@ -2849,9 +2969,7 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "optional": true + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, "node_modules/map-obj": { "version": "4.3.0", @@ -7076,9 +7194,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, "dependencies": { @@ -7440,30 +7558,45 @@ } }, "node_modules/ts-node": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", - "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", - "dev": true, - "optional": true, - "dependencies": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "source-map-support": "^0.5.17", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "bin": { "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" }, - "engines": { - "node": ">=10.0.0" - }, "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, "node_modules/tslib": { @@ -7486,17 +7619,15 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", - "dev": true, - "optional": true, + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -7512,6 +7643,12 @@ "node": ">=0.8.0" } }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "peer": true + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -7563,6 +7700,11 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -7745,8 +7887,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "optional": true, "engines": { "node": ">=6" } @@ -7870,6 +8010,13 @@ "requires": { "has-flag": "^4.0.0" } + }, + "typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "optional": true } } }, @@ -7953,6 +8100,14 @@ } } }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, "@endemolshinegroup/cosmiconfig-typescript-loader": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@endemolshinegroup/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.0.2.tgz", @@ -7964,6 +8119,42 @@ "make-error": "^1", "ts-node": "^9", "tslib": "^2" + }, + "dependencies": { + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "optional": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + } + } + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, "@nodelib/fs.scandir": { @@ -8270,12 +8461,41 @@ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "dev": true }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "@types/minimist": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, + "@types/node": { + "version": "20.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", + "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "peer": true, + "requires": { + "undici-types": "~5.25.1" + } + }, "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -8294,6 +8514,16 @@ "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -8354,9 +8584,7 @@ "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, "argv-formatter": { "version": "1.0.0", @@ -8753,9 +8981,7 @@ "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, "cross-spawn": { "version": "7.0.3", @@ -8902,9 +9128,7 @@ "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "optional": true + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" }, "dir-glob": { "version": "3.0.1", @@ -10001,9 +10225,7 @@ "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "optional": true + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, "map-obj": { "version": "4.3.0", @@ -13043,9 +13265,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "optional": true, "requires": { @@ -13349,17 +13571,22 @@ "dev": true }, "ts-node": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", - "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", - "dev": true, - "optional": true, - "requires": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "source-map-support": "^0.5.17", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" } }, @@ -13377,11 +13604,9 @@ "dev": true }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", - "dev": true, - "optional": true + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==" }, "uglify-js": { "version": "3.14.2", @@ -13390,6 +13615,12 @@ "dev": true, "optional": true }, + "undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "peer": true + }, "unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -13432,6 +13663,11 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -13576,9 +13812,7 @@ "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "optional": true + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 5a40377..a6ff401 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,11 @@ "description": "Home Assistant integration for interacting with First Bus", "main": "index.js", "scripts": { + "build": "tsc ./.build/*.ts --noEmit", "commit": "cz", - "release": "semantic-release" + "release": "semantic-release", + "test-unit": "python -m pytest tests/unit", + "test-integration": "python -m pytest tests/integration" }, "repository": { "type": "git", @@ -36,5 +39,9 @@ "hooks": { "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true" } + }, + "dependencies": { + "ts-node": "^10.9.1", + "typescript": "^5.2.2" } } diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..3793b25 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1 @@ +python -m pytest tests \ No newline at end of file diff --git a/run_unit_tests.sh b/run_unit_tests.sh new file mode 100644 index 0000000..9812533 --- /dev/null +++ b/run_unit_tests.sh @@ -0,0 +1 @@ +python -m pytest tests/unit \ No newline at end of file diff --git a/tests/integration/api_client/test_get_buses.py b/tests/integration/api_client/test_get_bus_times.py similarity index 95% rename from tests/integration/api_client/test_get_buses.py rename to tests/integration/api_client/test_get_bus_times.py index 7e14f2c..93f777a 100644 --- a/tests/integration/api_client/test_get_buses.py +++ b/tests/integration/api_client/test_get_bus_times.py @@ -14,7 +14,7 @@ async def test_when_get_buses_is_called_then_next_bus_is_returned(): for stop in stops: try: # Act - buses = await client.async_get_buses(stop) + buses = await client.async_get_bus_times(stop) assert buses is not None assert len(buses) > 0 diff --git a/tests/integration/utils/test_get_next_bus.py b/tests/integration/utils/test_get_next_bus.py index 228a25f..efe9257 100644 --- a/tests/integration/utils/test_get_next_bus.py +++ b/tests/integration/utils/test_get_next_bus.py @@ -5,7 +5,7 @@ from homeassistant.util.dt import (now) from custom_components.first_bus.api_client import FirstBusApiClient -from custom_components.first_bus.utils import (async_get_buses, get_next_bus) +from custom_components.first_bus.utils import (get_buses, get_next_bus) stops = ["0170SGB20116", "3800C509801", "2200YEA00934"] @@ -24,7 +24,8 @@ async def test_when_get_next_bus_is_called_then_next_bus_is_returned(target_buse for stop in stops: try: # Act - buses = await async_get_buses(client, stop, now()) + bus_times = await client.async_get_bus_times(stop) + buses = get_buses(bus_times, now()) assert buses is not None assert len(buses) > 0 diff --git a/tests/unit/config/__init__.py b/tests/unit/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/config/test_merge_config.py b/tests/unit/config/test_merge_config.py new file mode 100644 index 0000000..2a2b54b --- /dev/null +++ b/tests/unit/config/test_merge_config.py @@ -0,0 +1,102 @@ +import pytest +from custom_components.first_bus.config import (merge_config) +from custom_components.first_bus.const import CONFIG_BUSES, CONFIG_NAME, CONFIG_STOP + +@pytest.mark.asyncio +async def test_when_no_options_or_updated_config_supplied_then_no_changes_made(): + # Arrange + data = { + CONFIG_NAME: "name", + CONFIG_STOP: "stop", + CONFIG_BUSES: "buses" + } + options = None + updated_config = None + + # Act + config = merge_config(data, options, updated_config) + + # Assert + assert config is not None + assert config[CONFIG_NAME] == data[CONFIG_NAME] + assert config[CONFIG_STOP] == data[CONFIG_STOP] + assert config[CONFIG_BUSES] == data[CONFIG_BUSES] + +@pytest.mark.asyncio +async def test_when_options_supplied_then_option_changes_made(): + # Arrange + data = { + CONFIG_NAME: "name", + CONFIG_STOP: "stop", + CONFIG_BUSES: "buses" + } + options = { + CONFIG_NAME: "option name", + CONFIG_STOP: "option stop", + CONFIG_BUSES: "option buses" + } + updated_config = None + + # Act + config = merge_config(data, options, updated_config) + + # Assert + assert config is not None + assert config[CONFIG_NAME] == options[CONFIG_NAME] + assert config[CONFIG_STOP] == options[CONFIG_STOP] + assert config[CONFIG_BUSES] == options[CONFIG_BUSES] + +@pytest.mark.asyncio +async def test_when_updated_config_supplied_then_option_changes_made(): + # Arrange + data = { + CONFIG_NAME: "name", + CONFIG_STOP: "stop", + CONFIG_BUSES: "buses" + } + options = { + CONFIG_NAME: "option name", + CONFIG_STOP: "option stop", + CONFIG_BUSES: "option buses" + } + updated_config = { + CONFIG_NAME: "updated name", + CONFIG_STOP: "updated stop", + CONFIG_BUSES: "updated buses" + } + + # Act + config = merge_config(data, options, updated_config) + + # Assert + assert config is not None + assert config[CONFIG_NAME] == updated_config[CONFIG_NAME] + assert config[CONFIG_STOP] == updated_config[CONFIG_STOP] + assert config[CONFIG_BUSES] == updated_config[CONFIG_BUSES] + +@pytest.mark.asyncio +async def test_when_buses_not_updated_supplied_in_update_then_buses_removed(): + # Arrange + data = { + CONFIG_NAME: "name", + CONFIG_STOP: "stop", + CONFIG_BUSES: "buses" + } + options = { + CONFIG_NAME: "option name", + CONFIG_STOP: "option stop", + CONFIG_BUSES: "option buses" + } + updated_config = { + CONFIG_NAME: "updated name", + CONFIG_STOP: "updated stop", + } + + # Act + config = merge_config(data, options, updated_config) + + # Assert + assert config is not None + assert config[CONFIG_NAME] == updated_config[CONFIG_NAME] + assert config[CONFIG_STOP] == updated_config[CONFIG_STOP] + assert CONFIG_BUSES not in config \ No newline at end of file diff --git a/tests/unit/config/test_validate_config.py b/tests/unit/config/test_validate_config.py new file mode 100644 index 0000000..4428ca7 --- /dev/null +++ b/tests/unit/config/test_validate_config.py @@ -0,0 +1,127 @@ +import pytest +from custom_components.first_bus.config import (validate_config) +from custom_components.first_bus.const import CONFIG_NAME, CONFIG_STOP, CONFIG_BUSES + +@pytest.mark.asyncio +async def test_when_data_valid_then_no_errors_returned(): + # Arrange + original_config = { + CONFIG_NAME: "test", + CONFIG_STOP: "123", + CONFIG_BUSES: "12 A,12B,12" + } + + # Act + (errors, config) = validate_config(original_config) + + # Assert + assert CONFIG_NAME not in errors + assert CONFIG_STOP not in errors + assert CONFIG_BUSES not in errors + + assert CONFIG_NAME in config + assert config[CONFIG_NAME] == original_config[CONFIG_NAME] + assert CONFIG_STOP in config + assert config[CONFIG_STOP] == original_config[CONFIG_STOP] + assert CONFIG_BUSES in config + assert config[CONFIG_BUSES] == ["12 A", "12B", "12"] + +@pytest.mark.asyncio +async def test_when_buses_not_present_then_buses_empty_array(): + # Arrange + original_config = { + CONFIG_NAME: "test", + CONFIG_STOP: "123" + } + + # Act + (errors, config) = validate_config(original_config) + + # Assert + assert CONFIG_NAME not in errors + assert CONFIG_STOP not in errors + assert CONFIG_BUSES not in errors + + assert CONFIG_NAME in config + assert config[CONFIG_NAME] == original_config[CONFIG_NAME] + assert CONFIG_STOP in config + assert config[CONFIG_STOP] == original_config[CONFIG_STOP] + assert CONFIG_BUSES in config + assert config[CONFIG_BUSES] == [] + +@pytest.mark.asyncio +async def test_when_buses_none_then_buses_empty_array(): + # Arrange + original_config = { + CONFIG_NAME: "test", + CONFIG_STOP: "123", + CONFIG_BUSES: None + } + + # Act + (errors, config) = validate_config(original_config) + + # Assert + assert CONFIG_NAME not in errors + assert CONFIG_STOP not in errors + assert CONFIG_BUSES not in errors + + assert CONFIG_NAME in config + assert config[CONFIG_NAME] == original_config[CONFIG_NAME] + assert CONFIG_STOP in config + assert config[CONFIG_STOP] == original_config[CONFIG_STOP] + assert CONFIG_BUSES in config + assert config[CONFIG_BUSES] == [] + +@pytest.mark.asyncio +async def test_when_buses_empty_then_buses_empty_array(): + # Arrange + original_config = { + CONFIG_NAME: "test", + CONFIG_STOP: "123", + CONFIG_BUSES: "" + } + + # Act + (errors, config) = validate_config(original_config) + + # Assert + assert CONFIG_NAME not in errors + assert CONFIG_STOP not in errors + assert CONFIG_BUSES not in errors + + assert CONFIG_NAME in config + assert config[CONFIG_NAME] == original_config[CONFIG_NAME] + assert CONFIG_STOP in config + assert config[CONFIG_STOP] == original_config[CONFIG_STOP] + assert CONFIG_BUSES in config + assert config[CONFIG_BUSES] == [] + +@pytest.mark.asyncio +@pytest.mark.parametrize("bus_value",[ + ("A-B"), + ("12,12B,"), +]) +async def test_when_buses_not_valid_then_buses_empty_array(bus_value: str): + # Arrange + original_config = { + CONFIG_NAME: "test", + CONFIG_STOP: "123", + CONFIG_BUSES: bus_value + } + + # Act + (errors, config) = validate_config(original_config) + + # Assert + assert CONFIG_NAME not in errors + assert CONFIG_STOP not in errors + assert CONFIG_BUSES in errors + assert errors[CONFIG_BUSES] == "invalid_buses" + + assert CONFIG_NAME in config + assert config[CONFIG_NAME] == original_config[CONFIG_NAME] + assert CONFIG_STOP in config + assert config[CONFIG_STOP] == original_config[CONFIG_STOP] + assert CONFIG_BUSES in config + assert config[CONFIG_BUSES] == original_config[CONFIG_BUSES] \ No newline at end of file diff --git a/tests/unit/utils/test_get_buses.py b/tests/unit/utils/test_get_buses.py index 8023c98..0db1651 100644 --- a/tests/unit/utils/test_get_buses.py +++ b/tests/unit/utils/test_get_buses.py @@ -1,13 +1,7 @@ import pytest -from custom_components.first_bus.utils import (async_get_buses) from homeassistant.util.dt import (parse_datetime) -class MockedFirstBusApiClient: - def __init__(self, buses): - self._buses = buses - - async def async_get_buses(self, stop): - return self._buses +from custom_components.first_bus.utils import (get_buses) now = parse_datetime('2022-01-01T10:23:15+01:00') @@ -21,7 +15,7 @@ async def async_get_buses(self, stop): ]) async def test_when_get_buses_is_called_and_set_time_in_past_is_returned_then_due_timestamp_is_correct(raw_due, expected_due): # Arrange - raw_buses = [{ + bus_times = [{ 'ServiceRef': '0', 'ServiceNumber': '43', 'Destination': 'Newton Road Shops', @@ -29,16 +23,14 @@ async def test_when_get_buses_is_called_and_set_time_in_past_is_returned_then_du 'IsFG': 'N', 'IsLive': 'Y' }] - client = MockedFirstBusApiClient(raw_buses) - stop = "TEST_STOP" # Act - buses = await async_get_buses(client, stop, now) + buses = get_buses(bus_times, now) assert buses is not None assert len(buses) == 1 bus = buses[0] - raw_bus = raw_buses[0] + raw_bus = bus_times[0] # Assert assert "Due" in bus diff --git a/tests/unit/utils/test_get_next_bus.py b/tests/unit/utils/test_get_next_bus.py index 8bd3fa0..2d7ce33 100644 --- a/tests/unit/utils/test_get_next_bus.py +++ b/tests/unit/utils/test_get_next_bus.py @@ -1,8 +1,9 @@ import pytest from datetime import timedelta -from custom_components.first_bus.utils import (get_next_bus) from homeassistant.util.dt import (parse_datetime) +from custom_components.first_bus.utils import (get_next_bus) + now = parse_datetime('2022-01-01T10:00:00Z') @pytest.mark.asyncio @@ -24,12 +25,12 @@ async def test_when_get_next_bus_is_called_then_due_timestamp_is_correct(target_ 'IsLive': 'Y' }, { - 'ServiceRef': '0', - 'ServiceNumber': '43', - 'Destination': 'Newton Road Shops', - 'Due': now + timedelta(minutes=35), - 'IsFG': 'N', - 'IsLive': 'Y' + 'ServiceRef': '0', + 'ServiceNumber': '43', + 'Destination': 'Newton Road Shops', + 'Due': now + timedelta(minutes=35), + 'IsFG': 'N', + 'IsLive': 'Y' }, { 'ServiceRef': '0', @@ -94,6 +95,52 @@ async def test_when_get_next_bus_is_called_and_buses_in_the_past_then_correct_ne # Act next_bus = get_next_bus(buses, [], now) + # Assert + if (expected_next_bus is None): + assert next_bus is None + else: + assert next_bus is not None + + assert "Due" in next_bus + assert next_bus["Due"] == expected_next_bus["Due"] + + assert "ServiceNumber" in next_bus + assert next_bus["ServiceNumber"] == expected_next_bus["ServiceNumber"] + + assert "Destination" in next_bus + assert next_bus["Destination"] == expected_next_bus["Destination"] + + assert "IsFG" in next_bus + assert next_bus["IsFG"] == expected_next_bus["IsFG"] + + assert "IsLive" in next_bus + assert next_bus["IsLive"] == expected_next_bus["IsLive"] + +@pytest.mark.asyncio +async def test_when_subset_of_buses_looked_for_then_correct_bus_is_picked(): + # Arrange + buses = [{ + 'ServiceRef': '0', + 'ServiceNumber': '12 A', + 'Destination': 'Newton Road Shops', + 'Due': now - timedelta(minutes=35), + 'IsFG': 'N', + 'IsLive': 'Y' + }, + { + 'ServiceRef': '0', + 'ServiceNumber': '12', + 'Destination': 'Newton Road Shops', + 'Due': now + timedelta(minutes=64), + 'IsFG': 'N', + 'IsLive': 'Y' + }] + + expected_next_bus = buses[1] + + # Act + next_bus = get_next_bus(buses, ["12"], now) + # Assert if (expected_next_bus is None): assert next_bus is None