pax_global_header00006660000000000000000000000064147576721010014524gustar00rootroot0000000000000052 comment=56d830e67782e96168883fd9b65cf1fe549d5d78 pyMillLocal-0.4.0/000077500000000000000000000000001475767210100137465ustar00rootroot00000000000000pyMillLocal-0.4.0/.github/000077500000000000000000000000001475767210100153065ustar00rootroot00000000000000pyMillLocal-0.4.0/.github/dependabot.yml000066400000000000000000000004101475767210100201310ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: pip directory: "/" schedule: interval: weekly open-pull-requests-limit: 10 pyMillLocal-0.4.0/.github/workflows/000077500000000000000000000000001475767210100173435ustar00rootroot00000000000000pyMillLocal-0.4.0/.github/workflows/code_checker.yml000066400000000000000000000043271475767210100224720ustar00rootroot00000000000000name: Code checker on: push: pull_request: jobs: validate: runs-on: "ubuntu-latest" strategy: matrix: python-version: - "3.9" - "3.10" env: SRC_FOLDER: mill_local steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v3 with: path: ~/.cache/pip key: pip_cache - name: Install dependencies run: | pip install dlint flake8 flake8-bandit flake8-bugbear flake8-deprecated flake8-executable isort pylint pip install -r requirements.txt - name: isort if: github.event.pull_request.head.repo.full_name == github.repository run: | isort **/*.py - name: black if: github.event.pull_request.head.repo.full_name == github.repository uses: lgeiger/black-action@master with: args: . - name: Check for modified files if: github.event.pull_request.head.repo.full_name == github.repository id: git-check run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi) - name: Push changes if: github.event.pull_request.head.repo.full_name == github.repository && steps.git-check.outputs.modified == 'true' run: | git config --global user.name 'Daniel Hoyer' git config --global user.email 'mail@dahoiv.net' git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY git checkout $GITHUB_HEAD_REF git commit -am "fixup! Format Python code with black" git push - name: Flake8 Code Linter run: | flake8 $SRC_FOLDER --max-line-length=120 - name: isort run: | isort **/*.py - name: Pylint Code Linter run: | pylint --disable=C,R --enable=unidiomatic-typecheck $SRC_FOLDER - name: Run tests run: | pip install -r requirements-test.txt pytest -v -p no:logging -s pyMillLocal-0.4.0/.github/workflows/python-publish.yml000066400000000000000000000015151475767210100230550ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* pyMillLocal-0.4.0/.github/workflows/rebase.yml000066400000000000000000000011021475767210100213210ustar00rootroot00000000000000name: Automatic Rebase on: issue_comment: types: [created] jobs: rebase: name: Rebase if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') runs-on: ubuntu-latest steps: - name: Checkout the latest code uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 # otherwise, you will fail to push refs to dest repo - name: Automatic Rebase uses: cirrus-actions/rebase@1.8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pyMillLocal-0.4.0/.gitignore000066400000000000000000000034071475767210100157420ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ pyMillLocal-0.4.0/FUNDING.yml000066400000000000000000000001461475767210100155640ustar00rootroot00000000000000# These are supported funding model platforms github: Danielhiversen custom: http://paypal.me/dahoiv pyMillLocal-0.4.0/LICENSE000066400000000000000000000020671475767210100147600ustar00rootroot00000000000000MIT License Copyright (c) 2021 Daniel Hjelseth Høyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyMillLocal-0.4.0/README.md000066400000000000000000000022111475767210100152210ustar00rootroot00000000000000# Python module for local access to Gen 3 Mill heaters Python module for local access to the Gen 3 Mill Heaters using the [local REST API over WiFi](https://github.com/Mill-International-AS/Generation_3_REST_API). This python module is used to integrate Gen 3 Mill heaters into Home Assistant, see Mill Integration [documentation](https://www.home-assistant.io/integrations/mill/) and [source](https://github.com/home-assistant/core/tree/dev/homeassistant/components/mill). ## Implemented features Not all REST API endpoints are available through this module. These features are currently supported: - read device information summary (`GET /status`) - read detailed device state and control status (`GET /control-status`) - set target temperature for Normal type (`POST /set-temperature`) - set operation mode to `Control individually` and `Off` (`POST /operation-mode`) ## Install ```bash pip install mill_local ``` ## Contribution ### Install requirements ```bash pip install -r requirements.txt ``` ### Run tests ```bash pip install -r requirements-test.txt python -m pytest -v # with logging to STDOUT python -m pytest -v -p no:logging -s ``` pyMillLocal-0.4.0/mill_local/000077500000000000000000000000001475767210100160555ustar00rootroot00000000000000pyMillLocal-0.4.0/mill_local/__init__.py000066400000000000000000000141301475767210100201650ustar00rootroot00000000000000"""Local support for Mill wifi-enabled home heaters.""" import json import logging from enum import Enum from typing import Union import aiohttp.client_exceptions from aiohttp import ClientSession from async_timeout import timeout _LOGGER = logging.getLogger(__name__) class OperationMode(Enum): """Heater Operation Mode.""" # Follow the single set value, but not use any timers or weekly program CONTROL_INDIVIDUALLY = "Control individually" # The device is in off mode OFF = "OFF" # Follow the weekly program, referred to from the HA integration only in read-only mode WEEKLY_PROGRAM = "Weekly program" # Follow the single set value, with timers enabled INDEPENDENT_DEVICE = "Independent device" class OilHeaterPowerLevels(Enum): """Heater power setting by percentage.""" HIGH = 100 MEDIUM = 60 LOW = 40 OFF = 0 class Mill: """Mill data handler.""" def __init__(self, device_ip: str, websession: ClientSession, timeout_seconds: int = 15) -> None: """Init Mill data handler.""" self.device_ip = device_ip.replace("http://", "").replace("/", "").strip() self.websession = websession self.url = "http://" + self.device_ip self._timeout_seconds = timeout_seconds self._status = {} @property def version(self) -> str: """Return the API version.""" return self._status.get("version", "") @property def name(self) -> str: """Return heater name.""" return self._status.get("name", "") @property def mac_address(self) -> Union[str, None]: """Return heater MAC address.""" return self._status.get("mac_address") async def set_target_temperature(self, target_temperature: float) -> None: """Set target temperature.""" _LOGGER.debug("Setting target temperature to: '%s'", target_temperature) await self._post_request( command="set-temperature", payload={ "type": "Normal", "value": target_temperature, } ) async def set_operation_mode_control_individually(self) -> None: """Set operation mode to 'control individually'.""" await self._set_operation_mode(OperationMode.CONTROL_INDIVIDUALLY) async def set_operation_mode_off(self) -> None: """Set operation mode to 'off'.""" await self._set_operation_mode(OperationMode.OFF) async def connect(self) -> dict: """Connect to the device and return its status.""" return await self.get_status() async def get_status(self) -> dict: """Get status summary of the device.""" self._status = await self._get_request("status") return self._status async def fetch_heater_and_sensor_data(self) -> dict: """Get current heater state and control status.""" return await self._get_request("control-status") async def _set_operation_mode(self, mode: OperationMode) -> None: """Set heater operation mode.""" _LOGGER.debug("Setting operation mode to: '%s'", mode.value) await self._post_request(command="operation-mode", payload={"mode": mode.value}) async def _post_request(self, command: str, payload: dict) -> None: """HTTP POST request to Mill Local Api.""" async with timeout(self._timeout_seconds): async with self.websession.post( url=f"{self.url}/{command}", data=json.dumps(payload) ) as response: # Since body is not available when using raise_for_status=True, we use raise_for_status() json_response = await response.json() # Guard in case response body is missing and Error is raised if json_response is None: json_response = {"status": ""} try: response.raise_for_status() except aiohttp.ClientResponseError: _LOGGER.error( "POST request to '%s' failed with status code: '%s (%s)' and status message: '%s'", command, response.status, response.reason, json_response.get("status", "") # Guard in case status property is missing in body ) raise async def _get_request(self, command: str) -> Union[dict, None]: """HTTP GET request to Mill Local Api.""" async with timeout(self._timeout_seconds): async with self.websession.get( url=f"{self.url}/{command}" ) as response: # Since body is not available when using raise_for_status=True, we use raise_for_status() json_response = await response.json() # Guard in case response body is missing and Error is raised if json_response is None: json_response = {"status": ""} try: response.raise_for_status() return json_response except aiohttp.ClientResponseError: _LOGGER.error( "GET request to '%s' failed with status code: '%s (%s)' and status message: '%s'", command, response.status, response.reason, json_response.get("status", "") # Guard in case status property is missing in body ) raise class MillOilHeater(Mill): """Mill Oil Heater data handler.""" async def set_heater_power(self, power: OilHeaterPowerLevels) -> None: """Set oil oven heater power""" _LOGGER.debug("Setting oil oven heater power to: '%s'", power.value) await self._post_request( command="oil-heater-power", payload={ "heating_level_percentage": power.value, } ) async def fetch_heater_power_data(self) -> dict: """Get current heater state and control status.""" return await self._get_request("oil-heater-power") pyMillLocal-0.4.0/requirements-test.txt000066400000000000000000000000431475767210100202040ustar00rootroot00000000000000aioresponses pytest pytest-asyncio pyMillLocal-0.4.0/requirements.txt000066400000000000000000000000251475767210100172270ustar00rootroot00000000000000aiohttp async_timeoutpyMillLocal-0.4.0/setup.cfg000066400000000000000000000000421475767210100155630ustar00rootroot00000000000000[tool:pytest] asyncio_mode = auto pyMillLocal-0.4.0/setup.py000066400000000000000000000016511475767210100154630ustar00rootroot00000000000000import pathlib import pkg_resources from setuptools import setup with pathlib.Path("requirements.txt").open() as requirements_txt: install_requires = [ str(requirement) for requirement in pkg_resources.parse_requirements(requirements_txt) ] setup( name="mill_local", packages=["mill_local"], install_requires=install_requires, version="0.4.0", description="A python3 library to communicate with Mill heaters using local Gen 3 API", python_requires=">=3.8.0", author="Daniel Hjelseth Høyer", author_email="mail@dahoiv.net", url="https://github.com/Danielhiversen/pyMillLocal", license="MIT", classifiers=[ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ], ) pyMillLocal-0.4.0/tests/000077500000000000000000000000001475767210100151105ustar00rootroot00000000000000pyMillLocal-0.4.0/tests/__init__.py000066400000000000000000000000001475767210100172070ustar00rootroot00000000000000pyMillLocal-0.4.0/tests/conftest.py000066400000000000000000000026721475767210100173160ustar00rootroot00000000000000"""Test helpers """ import json import pathlib import pytest from aiohttp import ClientSession from aioresponses import aioresponses # See https://github.com/pnuckowski/aioresponses/issues/218 @pytest.fixture def mocked_response(): with aioresponses() as m: yield m @pytest.fixture(autouse=True) async def client_session(): """Fixture to execute asserts before and after a test is run""" # Setup client_session = ClientSession() yield client_session # Teardown await client_session.close() @pytest.fixture(scope="session") def status_command_response(): """A response for GET /status request.""" return load_fixture("status_command_response.json") @pytest.fixture(scope="session") def control_status_response(): """A response for GET /control-status call.""" return load_fixture("control_status_response.json") @pytest.fixture(scope="session") def generic_status_ok_response(): """A generic response with status OK.""" return load_fixture("generic_status_ok_response.json") @pytest.fixture(scope="session") def oil_heater_power_response(): """A generic response with status OK.""" return load_fixture("oil_heater_power_response.json") def load_fixture(name: str): """Load a fixture from disk.""" path = pathlib.Path(__file__).parent / "fixtures" / name content = path.read_text() if name.endswith(".json"): return json.loads(content) return content pyMillLocal-0.4.0/tests/fixtures/000077500000000000000000000000001475767210100167615ustar00rootroot00000000000000pyMillLocal-0.4.0/tests/fixtures/control_status_response.json000066400000000000000000000005541475767210100246610ustar00rootroot00000000000000{ "ambient_temperature": 14.9851131439209, "current_power": 336, "control_signal": 48, "lock_active": "No lock", "open_window_active_now": "Enabled not active now", "raw_ambient_temperature": 14.9851131439209, "set_temperature": 15, "switched_on": false, "connected_to_cloud": true, "operation_mode": "Control individually", "status": "ok" } pyMillLocal-0.4.0/tests/fixtures/generic_status_ok_response.json000066400000000000000000000000251475767210100252770ustar00rootroot00000000000000{ "status": "ok" } pyMillLocal-0.4.0/tests/fixtures/oil_heater_power_response.json000066400000000000000000000000451475767210100251200ustar00rootroot00000000000000{ "value": 100, "status": "ok" } pyMillLocal-0.4.0/tests/fixtures/status_command_response.json000066400000000000000000000002301475767210100246060ustar00rootroot00000000000000{ "name": "Mill panel", "custom_name": "", "version": "0x221017", "operation_key": "", "mac_address": "13:37:A6:5E:D3:CB", "status": "ok" } pyMillLocal-0.4.0/tests/test_init.py000066400000000000000000000310451475767210100174670ustar00rootroot00000000000000"""Test Mill.""" import json import pytest from aiohttp import ClientResponseError from mill_local import Mill, MillOilHeater, OperationMode, OilHeaterPowerLevels device_ip = "192.168.2.123" local_api_url = f"http://{device_ip}" async def test_init_when_websession_is_present(client_session): """Test Mill init and default values.""" mill = Mill(device_ip, client_session) # test default values assert mill.device_ip == device_ip assert mill.websession is not None assert mill.url == local_api_url assert mill._timeout_seconds == 15 assert mill.name == "" assert mill.version == "" assert mill.mac_address is None async def test_connect_when_successful(mocked_response, client_session, status_command_response): """Test successful connection to Mill device.""" mill = Mill(device_ip, client_session) mocked_response.get(f"{local_api_url}/status", status=200, payload=status_command_response) returned_data = await mill.connect() # assert returned data, we don't bother asserting every response property assert returned_data is not None assert len(returned_data.keys()) == 6 # assert data in Mill object assert mill.name == "Mill panel" assert mill.version == "0x221017" assert mill.mac_address == "13:37:A6:5E:D3:CB" async def test_connect_when_error_raised(mocked_response, client_session): """Test error raised when connecting to Mill device and None returned.""" mill = Mill(device_ip, client_session) mocked_response.get(f"{local_api_url}/status", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.connect() assert exp_400_info.value.status == 400 assert returned_data is None assert mill.name == "" assert mill.version == "" assert mill.mac_address is None async def test_fetch_heater_sensor_when_successful(mocked_response, client_session, control_status_response): """Test successful reading heater and sensor data.""" mill = Mill(device_ip, client_session) mocked_response.get(f"{local_api_url}/control-status", status=200, payload=control_status_response) returned_data = await mill.fetch_heater_and_sensor_data() assert returned_data is not None # we don't bother asserting every response property assert len(returned_data.keys()) == 11 async def test_fetch_heater_sensor_when_error_raised(mocked_response, client_session, control_status_response): """Test error raised when reading heater and sensor data and None returned.""" mill = Mill(device_ip, client_session) mocked_response.get(f"{local_api_url}/control-status", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.fetch_heater_and_sensor_data() assert exp_400_info.value.status == 400 assert returned_data is None async def test_set_target_temperature_when_successful(mocked_response, client_session, generic_status_ok_response): """Test successful setting the device target temperature.""" mill = Mill(device_ip, client_session) mocked_response.post(f"{local_api_url}/set-temperature", status=200, payload=generic_status_ok_response) returned_data = await mill.set_target_temperature(20.5) assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/set-temperature", method="POST", data=json.dumps({ "type": "Normal", "value": 20.5 })) async def test_set_target_temperature_when_error_raised(mocked_response, client_session, generic_status_ok_response): """Test error raised when setting the device target temperature.""" mill = Mill(device_ip, client_session) mocked_response.post(f"{local_api_url}/set-temperature", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.set_target_temperature(20.5) assert exp_400_info.value.status == 400 assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/set-temperature", method="POST", data=json.dumps({ "type": "Normal", "value": 20.5 })) async def test_set_heater_power_when_successful(mocked_response, client_session, generic_status_ok_response): """Test successful setting the device oil heater power.""" mill = MillOilHeater(device_ip, client_session) mocked_response.post(f"{local_api_url}/oil-heater-power", status=200, payload=generic_status_ok_response) returned_data = await mill.set_heater_power(OilHeaterPowerLevels.HIGH) assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/oil-heater-power", method="POST", data=json.dumps({ "heating_level_percentage": OilHeaterPowerLevels.HIGH.value, })) async def test_set_heater_power_when_error_raised(mocked_response, client_session, generic_status_ok_response): """Test error raised when setting the device oil heater power.""" mill = MillOilHeater(device_ip, client_session) mocked_response.post(f"{local_api_url}/oil-heater-power", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.set_heater_power(OilHeaterPowerLevels.HIGH) assert exp_400_info.value.status == 400 assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/oil-heater-power", method="POST", data=json.dumps({ "heating_level_percentage": OilHeaterPowerLevels.HIGH.value, })) async def test_fetch_heater_power_when_successful(mocked_response, client_session, oil_heater_power_response): """Test successful reading heater and sensor data.""" mill = MillOilHeater(device_ip, client_session) mocked_response.get(f"{local_api_url}/oil-heater-power", status=200, payload=oil_heater_power_response) returned_data = await mill.fetch_heater_power_data() assert returned_data is not None assert len(returned_data.keys()) == 2 assert type(returned_data.get("value")) is int async def test_fetch_heater_power_when_error_raised(mocked_response, client_session, oil_heater_power_response): """Test error raised when reading heater and sensor data and None returned.""" mill = MillOilHeater(device_ip, client_session) mocked_response.get(f"{local_api_url}/oil-heater-power", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.fetch_heater_power_data() assert exp_400_info.value.status == 400 assert returned_data is None async def test_set_operation_mode_control_individually_when_successful(mocked_response, client_session, generic_status_ok_response): """Test successful setting the device operation mode to 'control individually'.""" mill = Mill(device_ip, client_session) mocked_response.post(f"{local_api_url}/operation-mode", status=200, payload=generic_status_ok_response) returned_data = await mill.set_operation_mode_control_individually() assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/operation-mode", method="POST", data=json.dumps({ "mode": OperationMode.CONTROL_INDIVIDUALLY.value })) async def test_set_operation_mode_control_individually_when_error_raised(mocked_response, client_session, generic_status_ok_response): """Test error raised when setting the device operation mode to 'control individually'.""" mill = Mill(device_ip, client_session) mocked_response.post(f"{local_api_url}/operation-mode", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.set_operation_mode_control_individually() assert exp_400_info.value.status == 400 assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/operation-mode", method="POST", data=json.dumps({ "mode": OperationMode.CONTROL_INDIVIDUALLY.value })) async def test_set_operation_mode_off_when_successful(mocked_response, client_session, generic_status_ok_response): """Test successful setting the device operation mode to 'off'.""" mill = Mill(device_ip, client_session) mocked_response.post(f"{local_api_url}/operation-mode", status=200, payload=generic_status_ok_response) returned_data = await mill.set_operation_mode_off() assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/operation-mode", method="POST", data=json.dumps({ "mode": OperationMode.OFF.value })) async def test_set_operation_mode_off_when_error_raised(mocked_response, client_session, generic_status_ok_response): """Test error raised when setting the device operation mode to 'off'.""" mill = Mill(device_ip, client_session) mocked_response.post(f"{local_api_url}/operation-mode", status=400) with pytest.raises(ClientResponseError) as exp_400_info: returned_data = await mill.set_operation_mode_off() assert exp_400_info.value.status == 400 assert returned_data is None mocked_response.assert_called_once_with(url=f"{local_api_url}/operation-mode", method="POST", data=json.dumps({ "mode": OperationMode.OFF.value })) async def test_post_request_rais_error_on_400_and_500(mocked_response, client_session): """Test that get_request rais exception when status 400 or higher.""" mill = Mill(device_ip, client_session) # with response body mocked_response.post(f"{local_api_url}/operation-mode", status=400, body=json.dumps({ "status": "Failed to parse message body" })) # without response body mocked_response.post(f"{local_api_url}/operation-mode", status=500) with pytest.raises(ClientResponseError) as exp_400_info: await mill._post_request(command="operation-mode", payload={"mode": OperationMode.OFF.value}) assert exp_400_info.value.status == 400 with pytest.raises(ClientResponseError) as exp_500_info: await mill._post_request(command="operation-mode", payload={"mode": OperationMode.OFF.value}) assert exp_500_info.value.status == 500 async def test_get_request_rais_error_on_400_and_500(mocked_response, client_session): """Test that get_request rais exception when status 400 or higher.""" mill = Mill(device_ip, client_session) # with response body mocked_response.get(f"{local_api_url}/status", status=400, body=json.dumps({ "status": "Failed to parse message body" })) # without response body mocked_response.get(f"{local_api_url}/status", status=500) with pytest.raises(ClientResponseError) as exp_400_info: await mill._get_request("status") assert exp_400_info.value.status == 400 with pytest.raises(ClientResponseError) as exp_500_info: await mill._get_request("status") assert exp_500_info.value.status == 500