diff --git a/.coverage b/.coverage index b6882aa..1b90ea8 100644 Binary files a/.coverage and b/.coverage differ diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml index ed76d97..fbefdb4 100644 --- a/.github/workflows/lint_and_test.yml +++ b/.github/workflows/lint_and_test.yml @@ -22,3 +22,6 @@ jobs: run: | make lint make test + make black + make ruff + make mypy diff --git a/Makefile b/Makefile index 79dd224..198adca 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,18 @@ install: - pip install -r requirements.txt + pip install -r requirements.txt && pip install -r docs/requirements.txt lint: pylint src +mypy: + mypy src + +black: + black src tests + +ruff: + ruff --fix src tests && ruff format src tests + test: python -m pytest diff --git a/README.md b/README.md index 3f8b270..6342cec 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The library is an asyncio-driven library that interfaces with the Websocket API ### Requirements -- Python 3.9 or newer +- Python 3.11 or newer - websockets - asyncio diff --git a/docs/requirements.txt b/docs/requirements.txt index e052ba6..6c16e26 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,7 @@ +# autodoc +homeassistant +bluecurrent-api + # readthedocs dependencies alabaster==0.7.12 attrs==21.2.0 diff --git a/docs/source/_static/img/diagram.png b/docs/source/_static/img/diagram.png deleted file mode 100644 index c150fee..0000000 Binary files a/docs/source/_static/img/diagram.png and /dev/null differ diff --git a/docs/source/_static/img/step_card.png b/docs/source/_static/img/step_card.png deleted file mode 100644 index fbaa81f..0000000 Binary files a/docs/source/_static/img/step_card.png and /dev/null differ diff --git a/docs/source/_static/img/step_user.png b/docs/source/_static/img/step_user.png deleted file mode 100644 index 6439e9e..0000000 Binary files a/docs/source/_static/img/step_user.png and /dev/null differ diff --git a/docs/source/conf.py b/docs/source/conf.py index c4fd044..4a98746 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,6 +1,6 @@ import os import sys -sys.path.insert(0, os.path.abspath('../../src')) +sys.path.insert(0, os.path.abspath('../../')) # -- Project information ----------------------------------------------------- diff --git a/docs/source/documentation.md b/docs/source/documentation.md index 7527f45..e1e9797 100644 --- a/docs/source/documentation.md +++ b/docs/source/documentation.md @@ -5,11 +5,7 @@ maxdepth: 1 glob: --- -documentation/class-diagram documentation/api-package-example documentation/api-package-documentation documentation/integration-documentation -documentation/platforms-documentation -documentation/config-flow-documentation -documentation/automations-documentation ``` diff --git a/docs/source/documentation/api-package-documentation/client.md b/docs/source/documentation/api-package-documentation/client.md index 876b15f..96474a4 100644 --- a/docs/source/documentation/api-package-documentation/client.md +++ b/docs/source/documentation/api-package-documentation/client.md @@ -1,7 +1,7 @@ # Client ```{eval-rst} -.. automodule:: bluecurrent_api.client +.. automodule:: src.bluecurrent_api.client :members: :private-members: :special-members: diff --git a/docs/source/documentation/api-package-documentation/exceptions.md b/docs/source/documentation/api-package-documentation/exceptions.md index 6f97cdc..2667b13 100644 --- a/docs/source/documentation/api-package-documentation/exceptions.md +++ b/docs/source/documentation/api-package-documentation/exceptions.md @@ -1,7 +1,7 @@ # Exceptions ```{eval-rst} -.. automodule:: bluecurrent_api.exceptions +.. automodule:: src.bluecurrent_api.exceptions :members: :private-members: :special-members: diff --git a/docs/source/documentation/api-package-documentation/utils.md b/docs/source/documentation/api-package-documentation/utils.md index 83b9658..af27fda 100644 --- a/docs/source/documentation/api-package-documentation/utils.md +++ b/docs/source/documentation/api-package-documentation/utils.md @@ -1,7 +1,7 @@ # Utils ```{eval-rst} -.. automodule:: bluecurrent_api.utils +.. automodule:: src.bluecurrent_api.utils :members: :private-members: :special-members: diff --git a/docs/source/documentation/api-package-documentation/websocket.md b/docs/source/documentation/api-package-documentation/websocket.md index f048ff2..86f079c 100644 --- a/docs/source/documentation/api-package-documentation/websocket.md +++ b/docs/source/documentation/api-package-documentation/websocket.md @@ -1,7 +1,7 @@ # Websocket ```{eval-rst} -.. automodule:: bluecurrent_api.websocket +.. automodule:: src.bluecurrent_api.websocket :members: :private-members: :special-members: diff --git a/docs/source/documentation/api-package-example.md b/docs/source/documentation/api-package-example.md index 32f2eaa..d339e7e 100644 --- a/docs/source/documentation/api-package-example.md +++ b/docs/source/documentation/api-package-example.md @@ -2,12 +2,6 @@ To give a better view on how to package works here is an example on how to use the API package. -First an instance of the client is created. - -Then connection with the API is made with `connect` with an API token generated in the driver portal. - -And last with `asyncio.gather` the loop is started, and a receiver is given. All incoming messages will be routed to the given method. - ```python from bluecurrent_api import Client import asyncio @@ -27,6 +21,7 @@ async def main(): # example requests async def requests(): await client.get_charge_points() + await client.wait_for_response() await client.disconnect() # start the loop and send requests diff --git a/docs/source/documentation/automations-documentation.md b/docs/source/documentation/automations-documentation.md deleted file mode 100644 index 43a2342..0000000 --- a/docs/source/documentation/automations-documentation.md +++ /dev/null @@ -1,29 +0,0 @@ -# Automations - -[device automations](https://developers.home-assistant.io/docs/device_automation_index) - -## Triggers - -First the types are stored in the TYPES constants, Then trigger schemas are defined and stored in a tuple. - -### async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict[str, Any]] - -- Is called when opening the trigger dropdown of a charge point. -- Returns all triggers of a charge point. - -### async_attach_trigger(hass: HomeAssistant, config: ConfigType, action: TriggerActionType, trigger_info: TriggerInfo) -> CALLBACK_TYPE: - -- Is called when creating an automation with a charge point trigger in HA. - -## Conditions - -First the types are stored in the TYPES constants, Then trigger schemas are defined and stored in a tuple. - -### async_get_conditions(hass: HomeAssistant, device_id: str) -> list[dict[str, Any]] - -- Is called when opening the condition dropdown of a charge point. -- Returns all conditions of a charge point. - -### async_condition_from_config(hass: HomeAssistant, config: ConfigType) -> condition.ConditionCheckerType: - -- Is called when creating an automation with a charge point condition in HA. diff --git a/docs/source/documentation/class-diagram.md b/docs/source/documentation/class-diagram.md deleted file mode 100644 index 168dd32..0000000 --- a/docs/source/documentation/class-diagram.md +++ /dev/null @@ -1,9 +0,0 @@ -# Class Diagram - -```{image} /_static/img/diagram.png -:alt: class diagram of the integration and api package -:align: center -:scale: 20% -``` - -Click to enlarge diff --git a/docs/source/documentation/config-flow-documentation.md b/docs/source/documentation/config-flow-documentation.md deleted file mode 100644 index 0111380..0000000 --- a/docs/source/documentation/config-flow-documentation.md +++ /dev/null @@ -1,44 +0,0 @@ -# Config Flow - -## validate_input(client: Client, api_token: str) - -- Validates the given api token. - -## get_charge_cards(client: Client) -> list - -- Returns the charge card related to the api token given to validate_input. - -## ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN) - -VERSION = 1 - -- input: dict[str, Any] -- cards: list -- client = Client() - -### async_step_user(user_input: dict[str, Any] | None = None) -> FlowResult - -- This method is called when a user adds the integration to Home Assistant. If the user_input is None it returns the form with `async_show_form`. -- If there is user input it checks if the token matches the current one -- If there is no match the token will get validated and the user's login / email is requested. If an exception happens it will add an error to the `errors` dict and this will be given to `async_show_form` and shown to the user. -- If there are no errors it sets the unique id and stores the current entry if there is one. -- If the user checked the `use your own charge card` box the card step will be started. -- If there is a current entry 'update_entry' is called and the flow will be aborted because the reauth is successful. -- Otherwise, the entry is created normally. - -### async_step_card(user_input: dict[str, Any] | None = None) -> FlowResult - -- This method works in the same way as `async_step_user`, only this time the api call to get the charge cards needs to happen before the user is shown the form. -- If `cards` is empty, the cards will be requested from the API, if an exception happens they are added to the `errors` dict in the same way as `async_step_user`. -- If there are no errors the card names get added to a `schema` and shown to the user. -- When this method gets called again with a selected card name, the uid will get added to `input`. -- If an entry already exists it will be updated with `update_entry`. Otherwise, a config entry is created. - -### async_step_reauth async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: - -- Is called when the `ConfigEntryAuthFailed` exception is raised in `async_setup_entry` in `__init__.py`. -- Calls `async_step_user`. - -### update_entry(self) -> None: - -- Updates the existing entry and reload it. diff --git a/docs/source/documentation/integration-documentation.md b/docs/source/documentation/integration-documentation.md index cd6cfcd..fe8df5b 100644 --- a/docs/source/documentation/integration-documentation.md +++ b/docs/source/documentation/integration-documentation.md @@ -1,103 +1,9 @@ # Integration -### async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool - -- Retrieves the api token from the config_entry. -- Tries to connect to the API. -- Starts the loop. -- Waits until `CHARGE_POINTS` is received. -- Stores the connector, so it can be retrieved in other classes. -- Tells Ha to start the platform setup. -- Sets disconnect to be called when the integration is unloaded. -- Defines all service methods. -- Registers services. - -### async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool - -- Unloads the config entry. - -### set_entities_unavailable(hass: HomeAssistant, config_id: str) - -- Sets all BlueCurrent entities to unavailable. - -## Connector - -charge_points: dict[str, dict] -grid: dict[str, Any] - -### \_\_init\_\_(hass: HomeAssistant, config: ConfigEntry, client: Client) - -- Stores the given parameters. - -### connect(token: str) - -- Sets `on_data` as the receiver. -- Connects to the API. - -### on_data(message: dict) - -[](../flow/on-data.md) - -Routes the received message to the correct function based on the object. - -#### handle_charge_points(data: list) - -- Adds all evse_ids to `charge_points` and sends status requests for each. Also sends grid status request. - -### get_charge_point_data(self, evse_id: str) - -- Requests the status and settings of the charge point with the given evse_id. - -### add_charge_point(self, evse_id: str, model: str, name: str) - -- Adds the charge point to `charge_points` - -### update_charge_point(self, evse_id: str, data: dict) - -- Updates the key, value pairs of charge point with the given evse_id and dispatches a signal. - -#### handle_activity(data: dict) - -- The state of the block / operative switch is not returned with `CH_STATUS`, But the state is related to the state of the `activity` sensor. So this function sets the state of `block` switch based on the state of `activity`. - -### dispatch_value_update_signal(self, evse_id: str) -- Dispatches a `value_update` - -### dispatch_grid_update_signal(self) -> None: -- Dispatches a `grid_update` - -### start_loop() - -- Tries to start the loop for listening to the API. -- If an error happens, a warning is logged and the reconnect method is called after 1 second. - -```{note} -The reason for the one second delay is that the connection closes quite frequently so with this it 'immediately' reconnects. +```{toctree} +--- +maxdepth: 1 +glob: +--- +integration-documentation/* ``` - -### reconnect(self, event_time: datetime | None = None) - -- Tries to reconnect the API, starts the loop again and requests the charge points. -- If the reconnection fails the entities will be set unavailable and the reconnect method will be called again in 20 seconds. -- If the error was `request limit reached`, the reconnect method is called after the next limit reset. - -### disconnect() - -- Disconnects with the API, if the connection was already closed and a `WebsocketError` is thrown, ignore it. - -## BlueCurrentEntity(Entity) - -### \_\_init\_\_(connector: Connector, evse_id: str | None = None) - -- Stores connector -- If evse_id is given, device info is added to entity. - -### async_added_to_hass() - -- Connects the dispatcher to a signal to call `update` when triggered. - -Update method calls `update_from_latest_data()` and writes the state to HA. - -### update_from_latest_data() - -- Method that is implemented in the platform classes. diff --git a/docs/source/documentation/integration-documentation/config_flow.md b/docs/source/documentation/integration-documentation/config_flow.md new file mode 100644 index 0000000..571cae1 --- /dev/null +++ b/docs/source/documentation/integration-documentation/config_flow.md @@ -0,0 +1,23 @@ +# ConfigFlow + +```{eval-rst} +.. automodule:: homeassistant.components.blue_current.config_flow + :members: + :private-members: + :special-members: + :exclude-members: __weakref__ +``` + +### async_step_user +- This method is called when a user adds the integration to Home Assistant. If the user_input is None it returns the form with `async_show_form`. +- If there is user input it will validate the given token and get the customer id and email. The customer id is used as the unique id of the config entry and the email as title. +- If an exception happens it will add an error to the `errors` dict and this will be given to `async_show_form` and shown to the user. +- If there are no errors it sets the unique id and stores the current entry if there is one. +- If there is a current entry 'update_entry' is called and the flow will be aborted because the reauth is successful. +- Otherwise, the entry is created normally. + + +### async_step_reauth + +- Is called when a config is reconfigured. This button is shown when the `ConfigEntryAuthFailed` exception is raised in `async_setup_entry` in `__init__.py`. +- Calls `async_step_user` in which the entry is updated instead of creating a new one. \ No newline at end of file diff --git a/docs/source/documentation/integration-documentation/entity.md b/docs/source/documentation/integration-documentation/entity.md new file mode 100644 index 0000000..f6b1062 --- /dev/null +++ b/docs/source/documentation/integration-documentation/entity.md @@ -0,0 +1,9 @@ +# Entity + +```{eval-rst} +.. automodule:: homeassistant.components.blue_current.entity + :members: + :private-members: + :special-members: + :exclude-members: __weakref__ +``` \ No newline at end of file diff --git a/docs/source/documentation/integration-documentation/init.md b/docs/source/documentation/integration-documentation/init.md new file mode 100644 index 0000000..5cc60f1 --- /dev/null +++ b/docs/source/documentation/integration-documentation/init.md @@ -0,0 +1,9 @@ +# Init + +```{eval-rst} +.. automodule:: homeassistant.components.blue_current.__init__ + :members: + :private-members: + :special-members: + :exclude-members: __weakref__ +``` \ No newline at end of file diff --git a/docs/source/documentation/integration-documentation/sensor.md b/docs/source/documentation/integration-documentation/sensor.md new file mode 100644 index 0000000..fb934d5 --- /dev/null +++ b/docs/source/documentation/integration-documentation/sensor.md @@ -0,0 +1,9 @@ +# Sensor + +```{eval-rst} +.. automodule:: homeassistant.components.blue_current.sensor + :members: + :private-members: + :special-members: + :exclude-members: __weakref__ +``` \ No newline at end of file diff --git a/docs/source/documentation/platforms-documentation.md b/docs/source/documentation/platforms-documentation.md deleted file mode 100644 index fad4b1d..0000000 --- a/docs/source/documentation/platforms-documentation.md +++ /dev/null @@ -1,80 +0,0 @@ -# Platforms - -## ChargePointSensor(BlueCurrentEntity, SensorEntity) - -\_attr_should_poll = False - -### \_\_init\_\_(connector: Connector, sensor: SensorEntityDescription, evse_id: str) - -- Passes the connector and the evse_id to BlueCurrentEntity. - -### update_from_latest_data() - -- Is called when signal is dispatched. -- Gets the (potentially) new value from the connector class. -- If the value is not none, set the value and make the entity available. But if the entity is a timestamp entity (start_datetime, stop_datetime, offline_since) and the sensor value is not None or the new value is older than don't update the value. -- Else set the entity to unavailable except when the entity is a timestamp entity. - -See [](../notes.md) - -## GridSensor(SensorEntity) - -\_attr_should_poll = False - -### \_\_init\_\_(connector: Connector, sensor: SensorEntityDescription) - -- Initializes a grid sensor. - -### async_added_to_hass(self) -- Connects the dispatcher to a signal to call `update` when triggered. -Update method calls `update_from_latest_data()` and writes the state to HA. - -### update_from_latest_data() -- Updates the sensor value. -- If the value is None, it sets the sensor to unavailable. - -## ChargePointButton(BlueCurrentEntity, ButtonEntity): - -\_attr_should_poll = False - -### \_\_init\_\_(connector: Connector, evse_id: str, button: ButtonEntityDescription) - -- Passes the connector and the evse_id to BlueCurrentEntity. -- Sets the \_service, entity_description, unique_id and entity_id. - -### async_press() - -- Called when the button is pressed. -- Calls its service with an evse_id. - -### update_from_latest_data() - -- Not implemented. - -## ChargePointSwitch(BlueCurrentEntity, SwitchEntity) - -\_attr_should_poll = False - -### \_\_init\_\_(connector: Connector, evse_id: str, switch: dict[str, Any]) - -- This function differs from the other ones because of \_function it couldn't use SwitchEntityDescription. -- But it does the same as the `self.entity_description = ` only manually. - -### call_function(value: bool) - -- Calls the function to send the request to the API with the given value. Logs an error if there is no connection with the API. - -### async_turn_on() - -- Turns the setting on. - -### async_turn_off() - -- Turns the setting off - -### update_from_latest_data() - -- Is called when signal is dispatched. -- Gets the (potentially) new value from the connector class. -- If the value is not none, set the value and make the entity available. -- Else set the entity to unavailable. diff --git a/docs/source/file-overview/api-package-overview.md b/docs/source/file-overview/api-package-overview.md index 0b02b8f..8f6727d 100644 --- a/docs/source/file-overview/api-package-overview.md +++ b/docs/source/file-overview/api-package-overview.md @@ -6,6 +6,7 @@ HomeAssistantAPI ┃ ┣ bluecurrent_api ┃ ┃ ┣ client.py ┃ ┃ ┣ exceptions.py + ┃ ┃ ┣ py.typed ┃ ┃ ┣ utils.py ┃ ┃ ┣ websocket.py ┃ ┃ ┗ __init__.py @@ -25,6 +26,9 @@ Defines the module export. Defines the exceptions used in the API package. +### py.typed +Marker file used to tell Mypy (type checker) that this package has type hints. + ### client.py Contains the Client class that has all the 'public' methods of the API package. @@ -35,10 +39,4 @@ Defines the Websocket Class that handles the connection to the Websocket. ### utils.py -Contains methods for modifying incoming data. - -## Tests - -The test files contain tests for the file referenced in the name. - -See [](../testing/api-package-testing.md) +Contains methods for modifying incoming data. \ No newline at end of file diff --git a/docs/source/file-overview/integration-overview.md b/docs/source/file-overview/integration-overview.md index 2296d77..132e299 100644 --- a/docs/source/file-overview/integration-overview.md +++ b/docs/source/file-overview/integration-overview.md @@ -3,30 +3,22 @@ ``` homeassistant/components/bluecurrent ┣ translations - ┃ ┗ en.json - ┃ ┣ nl.json ┣ __init__.py ┣ button.py ┣ config_flow.py ┣ const.py - ┣ device_condition.py - ┣ device_trigger.py ┣ entity.py ┣ manifest.json ┣ sensor.py - ┣ services.yaml ┣ strings.json - ┗ switch.py tests/components/bluecurrent ┣ __init__.py + ┣ conftest.py ┣ test_button.py ┣ test_config_flow.py - ┣ test_device_condition.py - ┣ test_device_trigger.py ┣ test_init.py ┣ test_sensor.py - ┗ test_switch.py ``` ## Code @@ -38,16 +30,12 @@ Contains the strings for the config_flow that the user sees when adding the inte ### translations -Contains the strings for all languages. +Contains the strings for all languages. (Is generated from strings.json) ### manifest.json Defines information about the integration. -### services.json - -Defines all services. - ### const.py Contains constants used in the integration. @@ -80,10 +68,13 @@ Contains the BlueCurrentEntity class that all the platforms inherit. It also add ### \_\_init\_\_.py -Contains the method `init_integration` which starts the BlueCurrent integration with a single platform (sensor, switch or button) and test data. +Contains the method `init_integration` which starts the BlueCurrent integration with a single platform (sensor, switch or button), test data, and a mocked client. -### Other test files +### conftest.py +Used to store test fixtures. -The other test files contain tests for the file referenced in the name. +### test_config_flow +Tests for form and reauth -See [](../testing/integration-testing.md) +### test_sensor, test_button +tests for the specific platforms. \ No newline at end of file diff --git a/docs/source/flow/integration-flow.md b/docs/source/flow/integration-flow.md index 26f8bfc..a21d94c 100644 --- a/docs/source/flow/integration-flow.md +++ b/docs/source/flow/integration-flow.md @@ -2,28 +2,12 @@ ## Config Flow -The config flow gets started when a user adds the integration in Home Assistant. +The config flow gets started when a user adds the integration in Home Assistant from the integrations tab and entering their api token. -The user will first see a field for their API token and a checkbox for if they would like to use a charge cards. - -```{image} /_static/img/step_user.png -:alt: the config flow form -:align: center -:scale: 80% -``` - -If the box is checked another form will be shown where the user can select one of their charge cards. - -```{image} /_static/img/step_card.png -:alt: the card form -:align: center -:scale: 80% -``` ## Setup - After the config flow in completed the method `async_setup_entry` in \_\_init\_\_.py gets called by Home Assistant. -Here it retrieves the API token from the config_entry. And tries to connect to the API. If the connection is successful the `loop` gets started, then it waits until `CHARGE_POINTS` is received from the API and tells Home Assistant to start the platform setups with `async_setup_platforms`. +Here it retrieves the API token from the config_entry. And validates it. If the validation is successful a background task to connect and listen to the websocket is started, then the function waits until `CHARGE_POINTS` is received from the API and tells Home Assistant to start the platform setups with `async_setup_platforms`. ## Platforms diff --git a/docs/source/flow/on-data.md b/docs/source/flow/on-data.md index 5628b17..3182c29 100644 --- a/docs/source/flow/on-data.md +++ b/docs/source/flow/on-data.md @@ -7,6 +7,9 @@ This method then routes the message to the correct function based on the object_ If the object name is `CHARGE_POINTS` the method `handle_charge_points` will be called. This method adds all evse_ids to the charge_points dict, sends status requests for all evse_ids and sends a grid status request. +## CHARGE_CARDS +If the object name is `CHARGE_CARDS` the cards will be stored in the connector to be used in selects such as start session. + ## VALUE_TYPES If the object name is `CH_STATUS` or `CH_SETTINGS` the key value pairs in the data will be added / updated in the charge_points dict for the evse_id. Then the `value_update` signal is dispatched with the evse_id. diff --git a/docs/source/index.md b/docs/source/index.md index 152158c..7a3e7f9 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,8 +1,6 @@ # Welcome to the Blue Current HA integration Docs! -Here you can find information on the documentation system used at Blue Current. - -For instructions on setting up the system, see [](setting-up). +For instructions on setting up the code base, see [](setting-up). For an overview of all the files, see [](file-overview). @@ -10,8 +8,6 @@ For an explanation for the integration flow, see [](flow). For the documentation of all the functions see [](documentation). -For documentation on testing see [](testing). - For examples of objects received from the api package see [](data-models) ## Complementary documentation @@ -31,6 +27,5 @@ file-overview flow documentation data-models -testing notes ``` diff --git a/docs/source/notes.md b/docs/source/notes.md index c2870ea..9487561 100644 --- a/docs/source/notes.md +++ b/docs/source/notes.md @@ -14,18 +14,10 @@ In the dev docs Home Assistant uses the word 'platform' for different things. Device info is added to entities and not the other way around. -## Dummy Message - -When the message `STATUS_START_SESSION` is received a dummy `CH_STATUS` message is send to the receiver method. This is because the value of start_datetime send with the next pushed `CH_STATUS` is the timestamp of the session before. - -In Home Assistant the value of the start_datetime sensor is only updated when the new timestamp is newer than its value. - -This does mean that the start_datetime value is not accurate but most of the time the difference is under 10 seconds. - ## Error handling If something goes wrong server side an `ERROR` object is sent and an `WebsocketError` is raised in the api package. Setting and Action request can also have an error in their `RECEIVED_` or `STATUS_` messages, but this is sent to HA and logged. ## API and AUTH token -To use this the api package the user first validates his api token with `VALIDATE_API_TOKEN` (token can be generated in driver portal -> developer mode -> home assistant api token). If the token is used, the auth token is returned and stored in `auth_token` in `websocket.py` all other requests add this token for authentication. +To use this the api package the user first validates his api token with `VALIDATE_API_TOKEN` (token can be generated in my.bluecurrent.nl -> advanced -> home assistant api token). If the token is used, the auth token is returned and stored in `auth_token` in `websocket.py` all other requests add this token for authentication. diff --git a/docs/source/setting-up/api-package-setup.md b/docs/source/setting-up/api-package-setup.md index 26662bd..eb08f7a 100644 --- a/docs/source/setting-up/api-package-setup.md +++ b/docs/source/setting-up/api-package-setup.md @@ -1,13 +1,10 @@ # Setting up the API package -To get started developing the API package, you will have to install a few packages/programs. Depending on the -programming languages and tools you use, some configuration will be required. - ## General requirements to develop on the API package, the following tools are needed: -1. Python >= 3.8 +1. Python >= 3.11 2. python modules (no need to install these manually): - websockets - pyzt @@ -20,20 +17,31 @@ to develop on the API package, the following tools are needed: ## Make commands +```{note} +Make commands can be used on Windows with GnuWin or just running the commands defined in the make file. +``` + ### installing 1. `git clone https://github.com/bluecurrent/HomeAssistantAPI.git` -2. `make install` +2. `python -m venv venv` +3. `make install` ### testing - `make test` +- `make test-cov` + +### lint, format, typechecking - `make lint` +- `make ruff` +- `make black` +- `make mypy` ### publishing 1. `make build` -2. `make publish` +2. `make publish` (credentials stored in password manager) ### build documentation @@ -44,15 +52,20 @@ to develop on the API package, the following tools are needed: 1. `make build` 2. `pip install bluecurrent-api--py3-none-any.whl` -### Using newer build of the API package in HA container (without publishing it). +### Using newer build of the API package in a HA dev container (without publishing it). + +1. move the .whl file to the container with `docker cp dist/bluecurrent-api--py3-none-any.whl` +2. install the package inside the container +3. remove the .whl file. + +### Publish checklist +1. Run tests, lint, format and type checker. +2. Increment version in pyproject.toml. +3. make build +4. For large changes install new package in Ha container and run integration tests and start ha and test manually. +5. Push to git +6. Wait until checks are successful. +7. make publish + -1. Copy the `ha_package_installer_sample.bat` and rename it to `ha_package_installer.bat`. - - If you are using Linux you can change the script to bash (keep the name ha_package_installer, so it will be ignored by git). -2. Change the variables: - - container: the name of the HA devcontainer - - version: the version of package -3. Run the script. -```{note} -bluecurrent_api--py3-none-any.whl needs to match the .whl file in dist/ after make build -``` diff --git a/docs/source/setting-up/integration-setup.md b/docs/source/setting-up/integration-setup.md index 9de87d1..c31ccf0 100644 --- a/docs/source/setting-up/integration-setup.md +++ b/docs/source/setting-up/integration-setup.md @@ -12,17 +12,21 @@ The important ones are: - Run Home Assistant Core - Generate requirements -- Install all requirements -- Install all test requirements - -- pytest / pylint (Do not use these because they will run over the whole code) +- Code Coverage ## Installing [Set up Development Environment](https://developers.home-assistant.io/docs/development_environment) -1. run task `Generate requirements` -2. run task `Install all requirements` +install minimal requirements +- `pip install -r requirements.txt` +- `pip install -r requirements_test.txt` +- `pip install -r requirements_test_pre_commit.txt` + +- Run task `Run Home Assistant Core` to install remaining requirements. + +- install bluecurrent-api +- check if it works by running the `Code coverage` task for blue_current. ## Running @@ -33,18 +37,21 @@ The important ones are: [Testing your code](https://developers.home-assistant.io/docs/development_testing) -1. run task `Install all Test Requirements` -2. `pytest tests/components/bluecurrent/` - -### With coverage +- `pytest tests/components/blue_current/` +- Run task `Code coverage` (integration can be specified) -- `pytest tests/components/bluecurrent/ --cov=homeassistant.components. --cov-report term-missing -vv` - -- Or run task `coverage` (integration can be specified) +### Debugging tests +- Go to the Testing tab in VScode. +- configure tests and select pylint +- in .vscode/settings.json set `python.testing.pytestArgs` to `["tests/components/blue_current"]` ## Lint - -- pylint homeassistant/components/bluecurrent +- pylint homeassistant/components/blue_current tests/components/blue_current +- ruff --fix homeassistant/components/blue_current tests/components/blue_current +- ruff format homeassistant/components/blue_current tests/components/blue_current +- black homeassistant/components/blue_current tests/components/blue_current +- mypy homeassistant/components/blue_current tests/components/blue_current +(Tests do not have to be error free for mypy and pylint) ## Submitting changes @@ -69,4 +76,6 @@ A new repo called ha-bluecurrent was added because the pr to the official Home A With this repo the integration could be added to Home Assistant with [HACS](https://hacs.xyz/). -Right now the [url](https://github.com/bluecurrent/ha-bluecurrent) of the repo can be added as a custom integration to HACS. In the future it could also be added to hacs itself. \ No newline at end of file +Right now the [url](https://github.com/bluecurrent/ha-bluecurrent) of the repo can be added as a custom integration to HACS. In the future it could also be added to hacs itself. + +This repo will be deprecated when the integration is fully added official Home Assistant repo. \ No newline at end of file diff --git a/docs/source/testing.md b/docs/source/testing.md deleted file mode 100644 index 86f9eda..0000000 --- a/docs/source/testing.md +++ /dev/null @@ -1,9 +0,0 @@ -# Testing - -```{toctree} ---- -maxdepth: 1 -glob: ---- -testing/* -``` diff --git a/docs/source/testing/api-package-testing.md b/docs/source/testing/api-package-testing.md deleted file mode 100644 index 2666387..0000000 --- a/docs/source/testing/api-package-testing.md +++ /dev/null @@ -1,78 +0,0 @@ -## Api Package - -Every function / method has one test function. In this function are multiple tests. - -### Async Functions - -When testing an asynchronous the `@pytest.mark.asyncio` mark is used. - -### Mocking / Patching - -When something needs to be mocked or patched a MockerFixture is passed to the test function. - -### disabling function - -```python -mocker.patch('src.bluecurrent_api.websocket.Websocket._connect') -``` - -### changing return value - -```python - mocker.patch( - 'src.bluecurrent_api.websocket.Websocket._recv', - return_value={"object": "STATUS_API_TOKEN", - "success": True, "token": "abc"} - ) -``` - -### Checking if function is called - -```python - test_send_request = mocker.patch("src.bluecurrent_api.client.Websocket.send_request") - - test_send_request.assert_called_with( - {'command': 'START_SESSION', 'evse_id': '101', 'session_token': '123'}) -``` - -### Checking if exception is raised - -```python -with pytest.raises(WebsockerError): - await websocket.get_charge_cards() -``` - -### Mocking an attribute or method of an object - -```python -mocker.patch.object(Websocket, '_connection') - - -mock_send = mocker.patch.object(Websocket, '_send') -``` - -### AsyncMock - -When a test needs to check if an async function is called an AsyncMock is used. - -```python -test_close = mocker.patch( - 'src.bluecurrent_api.websocket.Websocket._connection.close', - create=True, - side_effect=AsyncMock() - ) -``` - -### test_send_to_receiver - -In this test the receiver is mocked and then changed to an AsyncMock - -```python - mock_receiver = mocker.MagicMock() - ... - mock_receiver.assert_called_with('test') - - async_mock_receiver = AsyncMock() - ... - async_mock_receiver.assert_called_with('test') -``` diff --git a/docs/source/testing/integration-testing.md b/docs/source/testing/integration-testing.md deleted file mode 100644 index 09bb21f..0000000 --- a/docs/source/testing/integration-testing.md +++ /dev/null @@ -1,39 +0,0 @@ -## Integration - -### \_\_init\_\_.py - -Contains the method `init_integration` which starts the BlueCurrent integration with a single platform (sensor, switch or button) and test data. Also, the whole bluecurrent api is patched here instead of in every test method. - -### MockConfigEntry - -MockConfigEntry is used to mock a config entry - -### HomeAssistant - -Most test methods have Home Assistant passed to them. - -### states and entity registry - -To check the state of an entity: - -```python - state = hass.states.get(f"button.{button}_101") -``` - -To get the entity - -```python - entity_registry.async_get(f"button.{button}_101") -``` - -### Platform tests - -All platforms have two tests one to check if all entities are created and one if the value is updated. - -### test_config_flow - -The config flow needs to have 100% code coverage. - -### Automation testing - -The code could be made smaller but it works. And other integrations also have it like this. diff --git a/ha_package_installer_sample.bat b/ha_package_installer_sample.bat deleted file mode 100644 index f32440e..0000000 --- a/ha_package_installer_sample.bat +++ /dev/null @@ -1,22 +0,0 @@ -:: This script is an sample for installing an unreleased api package into the Ha devcontainer. -:: To use, please copy this file and name it ha_package_installer.* so it will be ignored by git and change the variables. -:: If you are on Linux and cannot run batch scripts, copy the commands and create a bash script. (named ha_package_installer) - -@echo off - -:: CHANGE ME -set container="CONTAINER" -set version="VERSION" - -:: build the package -make build - - -:: copy the .whl file to the HA container -docker cp dist\bluecurrent_api-%version%-py3-none-any.whl %container%:/workspaces/core/ - -:: install the package in the container -docker exec %container% pip install core/bluecurrent_api-%version%-py3-none-any.whl --no-deps --force-reinstall - -:: delete the .whl file -docker exec %container% rm core/bluecurrent_api-%version%-py3-none-any.whl \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e0f8ede..3cd6c9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,7 @@ pytest-mock pytest-cov pytest-asyncio coverage -build \ No newline at end of file +build +ruff +black +mypy \ No newline at end of file diff --git a/src/bluecurrent_api/utils.py b/src/bluecurrent_api/utils.py index a117856..b88488e 100644 --- a/src/bluecurrent_api/utils.py +++ b/src/bluecurrent_api/utils.py @@ -19,7 +19,7 @@ SMART_CHARGING: set[str] = set() -def calculate_average_usage_from_phases(phases: list[float]) -> float: +def calculate_average_usage_from_phases(phases: list[float | None]) -> float: """Get the average of the phases that are not 0.""" used_phases = [p for p in phases if p] if len(used_phases): diff --git a/tests/test_client.py b/tests/test_client.py index b6f7abe..1ce697e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,4 +1,3 @@ -from datetime import timedelta from src.bluecurrent_api.client import Client import pytest from pytest_mock import MockerFixture diff --git a/tests/test_utils.py b/tests/test_utils.py index 5f268da..7f46bcc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -24,31 +24,31 @@ def test_calculate_total_from_phases(): - total = calculate_average_usage_from_phases((10, 10, 10)) + total = calculate_average_usage_from_phases([10, 10, 10]) assert total == 10 - total = calculate_average_usage_from_phases((0, 0, 0)) + total = calculate_average_usage_from_phases([0, 0, 0]) assert total == 0 - total = calculate_average_usage_from_phases((10, None, 20)) + total = calculate_average_usage_from_phases([10, None, 20]) assert total == 15 - total = calculate_average_usage_from_phases((10, 6, 10)) + total = calculate_average_usage_from_phases([10, 6, 10]) assert total == 8.7 - total = calculate_average_usage_from_phases((10, 8, 1)) + total = calculate_average_usage_from_phases([10, 8, 1]) assert total == 6.3 - total = calculate_average_usage_from_phases((10, 0, 20)) + total = calculate_average_usage_from_phases([10, 0, 20]) assert total == 15 - total = calculate_average_usage_from_phases((5, 0, 0)) + total = calculate_average_usage_from_phases([5, 0, 0]) assert total == 5 - total = calculate_average_usage_from_phases((6, None, None)) + total = calculate_average_usage_from_phases([6, None, None]) assert total == 6 - total = calculate_average_usage_from_phases((None, None, None)) + total = calculate_average_usage_from_phases([None, None, None]) assert total == 0 diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 0551269..2a18ba7 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -1,7 +1,11 @@ from unittest.mock import MagicMock from websockets.client import WebSocketClientProtocol from websockets.legacy.client import Connect -from websockets.exceptions import InvalidStatusCode, ConnectionClosedError, WebSocketException +from websockets.exceptions import ( + InvalidStatusCode, + ConnectionClosedError, + WebSocketException, +) from websockets.frames import Close from src.bluecurrent_api.websocket import ( @@ -29,7 +33,7 @@ async def test_start(mocker: MockerFixture): with pytest.raises(WebsocketError): await websocket.start(mock_receiver, mock_on_open) - websocket.auth_token = '123' + websocket.auth_token = "123" await websocket.start(mock_receiver, mock_on_open) mock__loop.assert_called_once_with(mock_receiver, mock_on_open) @@ -76,7 +80,7 @@ async def test__send_recv_single_message(mocker: MockerFixture): err = WebSocketException() mock_ws.recv.side_effect = err - with pytest.raises(WebsocketError): + with pytest.raises(WebsocketError): await websocket._send_recv_single_message({})