diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 5c574945..00000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[report] -exclude_lines = - raise NotImplementedError - @abstract - pragma: no cover diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66e1c357..ae494a57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: include: - - ha-version: '2023.2' + - ha-version: '2023.7' latest: true steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index dd26de82..60ef0d9f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * Мобильное приложение [Дом с Алисой](https://mobile.yandex.ru/apps/smarthome) * Веб-интерфейс [Квазар](https://yandex.ru/quasar/iot) -Компонент работает на Home Assistant версии **2023.2** или новее. +Компонент работает на Home Assistant версии **2023.7** или новее. ## Ссылки * [**Документация**](https://docs.yaha-cloud.ru/v0.6.x/) diff --git a/docs/index.md b/docs/index.md index 544bf144..98667dd5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ * Мобильное приложение [Дом с Алисой](https://mobile.yandex.ru/apps/smarthome) * Веб-интерфейс [Квазар](https://yandex.ru/quasar/iot) -Компонент работает на Home Assistant версии **2023.2** или новее. +Компонент работает на Home Assistant версии **2023.7** или новее. ## Полезные ссылки * [Чат по компоненту в Телеграме :fontawesome-brands-telegram:](https://t.me/yandex_smart_home) diff --git a/hacs.json b/hacs.json index 6ae5627f..2524ff1c 100644 --- a/hacs.json +++ b/hacs.json @@ -1,6 +1,6 @@ { "country": ["RU", "BY"], - "homeassistant": "2023.2", + "homeassistant": "2023.7", "name": "Yandex Smart Home", "render_readme": true } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..fb3535ee --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.coverage.report] +exclude_also = [ + "raise NotImplementedError", + "@abstractmethod" +] diff --git a/tests/conftest.py b/tests/conftest.py index 333f69bd..b3604fbf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component import pytest -from pytest_homeassistant_custom_component.common import MockConfigEntry +from pytest_homeassistant_custom_component.common import MockConfigEntry, MockUser from custom_components import yandex_smart_home from custom_components.yandex_smart_home import DOMAIN, async_setup, async_setup_entry, const @@ -20,7 +20,7 @@ pytest_plugins = 'pytest_homeassistant_custom_component' -def pytest_configure(config): +def pytest_configure(*_): yandex_smart_home._PYTEST = True @@ -29,6 +29,15 @@ def auto_enable_custom_integrations(enable_custom_integrations): yield +@pytest.fixture(name='skip_notifications', autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch('homeassistant.components.persistent_notification.async_create'), patch( + 'homeassistant.components.persistent_notification.async_dismiss' + ): + yield + + @pytest.fixture def config_entry(): return MockConfigEntry( @@ -42,7 +51,7 @@ def config_entry(): @pytest.fixture -def config_entry_with_notifier(hass_admin_user): +def config_entry_with_notifier(hass_admin_user: MockUser): return MockConfigEntry(domain=DOMAIN, data={const.CONF_NOTIFIER: [{ const.CONF_NOTIFIER_OAUTH_TOKEN: '', const.CONF_NOTIFIER_SKILL_ID: '', @@ -70,8 +79,8 @@ def config_entry_cloud_connection(): @pytest.fixture def hass_platform(event_loop: asyncio.AbstractEventLoop, hass, config_entry): demo_sensor = DemoSensor( - unique_id='outside_temp', - name='Outside Temperature', + 'outside_temp', + 'Outside Temperature', state=15.6, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -80,29 +89,24 @@ def hass_platform(event_loop: asyncio.AbstractEventLoop, hass, config_entry): ) demo_sensor.hass = hass demo_sensor.entity_id = 'sensor.outside_temp' + demo_sensor._attr_name = 'Outside Temperature' demo_light = DemoLight( - unique_id='light_kitchen', - name='Kitchen Light', + 'light_kitchen', + 'Kitchen Light', available=True, state=True, ) demo_light.hass = hass demo_light.entity_id = 'light.kitchen' + demo_light._attr_name = 'Kitchen Light' - event_loop.run_until_complete( - demo_sensor.async_update_ha_state() - ) - event_loop.run_until_complete( - demo_light.async_update_ha_state() - ) + demo_sensor.async_write_ha_state() + demo_light.async_write_ha_state() event_loop.run_until_complete( async_setup_component(hass, http.DOMAIN, {http.DOMAIN: {}}) ) - event_loop.run_until_complete( - hass.async_block_till_done() - ) config_entry.add_to_hass(hass) with patch.object(hass.config_entries.flow, 'async_init', return_value=None): @@ -115,8 +119,8 @@ def hass_platform(event_loop: asyncio.AbstractEventLoop, hass, config_entry): @pytest.fixture def hass_platform_cloud_connection(event_loop: asyncio.AbstractEventLoop, hass, config_entry_cloud_connection): demo_sensor = DemoSensor( - unique_id='outside_temp', - name='Outside Temperature', + 'outside_temp', + 'Outside Temperature', state=15.6, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -125,42 +129,36 @@ def hass_platform_cloud_connection(event_loop: asyncio.AbstractEventLoop, hass, ) demo_sensor.hass = hass demo_sensor.entity_id = 'sensor.outside_temp' + demo_sensor._attr_name = 'Outside Temperature' demo_binary_sensor = DemoBinarySensor( - unique_id='front_door', - name='Front Door', + 'front_door', + 'Front Door', state=True, device_class=BinarySensorDeviceClass.DOOR, ) demo_binary_sensor.hass = hass demo_binary_sensor.entity_id = 'binary_sensor.front_Door' + demo_binary_sensor._attr_name = 'Front Door' demo_light = DemoLight( - unique_id='light_kitchen', - name='Kitchen Light', + 'light_kitchen', + 'Kitchen Light', ct=240, available=True, state=True, ) demo_light.hass = hass demo_light.entity_id = 'light.kitchen' + demo_light._attr_name = 'Kitchen Light' - event_loop.run_until_complete( - demo_sensor.async_update_ha_state() - ) - event_loop.run_until_complete( - demo_binary_sensor.async_update_ha_state() - ) - event_loop.run_until_complete( - demo_light.async_update_ha_state() - ) + demo_sensor.async_write_ha_state() + demo_binary_sensor.async_write_ha_state() + demo_light.async_write_ha_state() event_loop.run_until_complete( async_setup_component(hass, http.DOMAIN, {http.DOMAIN: {}}) ) - event_loop.run_until_complete( - hass.async_block_till_done() - ) config_entry_cloud_connection.add_to_hass(hass) event_loop.run_until_complete(async_setup(hass, {})) @@ -169,12 +167,3 @@ def hass_platform_cloud_connection(event_loop: asyncio.AbstractEventLoop, hass, event_loop.run_until_complete(hass.async_block_till_done()) return hass - - -@pytest.fixture(name='skip_notifications', autouse=True) -def skip_notifications_fixture(): - """Skip notification calls.""" - with patch('homeassistant.components.persistent_notification.async_create'), patch( - 'homeassistant.components.persistent_notification.async_dismiss' - ): - yield diff --git a/tests/requirements_2023.7.txt b/tests/requirements_2023.7.txt new file mode 100644 index 00000000..0da5037f --- /dev/null +++ b/tests/requirements_2023.7.txt @@ -0,0 +1,4 @@ +pytest-homeassistant-custom-component==0.13.42 +aiohttp-cors==0.7.0 +home_assistant_intents==2023.7.24 +hassil==1.2.0 diff --git a/tests/test_capability.py b/tests/test_capability.py index 30a8e803..fda2c3d3 100644 --- a/tests/test_capability.py +++ b/tests/test_capability.py @@ -1,9 +1,11 @@ from __future__ import annotations from typing import Any, Optional +from unittest.mock import patch +from homeassistant import core from homeassistant.components import button, climate, cover, fan, humidifier, light, lock, media_player, switch -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_ON, Platform from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -111,13 +113,19 @@ async def set_state(self, data, state): async def test_capability_demo_platform(hass): - components = [button, switch, light, cover, media_player, fan, climate, humidifier, lock] - - for component in components: - await async_setup_component( - hass, component.DOMAIN, {component.DOMAIN: [{'platform': 'demo'}]} - ) - await hass.async_block_till_done() + with patch( + 'homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM', + [ + Platform.BUTTON, Platform.SWITCH, Platform.LIGHT, Platform.COVER, Platform.MEDIA_PLAYER, Platform.FAN, + Platform.CLIMATE, Platform.HUMIDIFIER, Platform.LOCK + ], + ): + await async_setup_component(hass, core.DOMAIN, {}) + for component in [button, switch, light, cover, media_player, fan, climate, humidifier, lock]: + await async_setup_component( + hass, component.DOMAIN, {component.DOMAIN: [{'platform': 'demo'}]} + ) + await hass.async_block_till_done() # for x in sorted(hass.states.async_all(), key=lambda e: e.entity_id): # e = YandexEntity(hass, BASIC_CONFIG, x) diff --git a/tests/test_cloud.py b/tests/test_cloud.py index ed5055e5..1d36dbff 100644 --- a/tests/test_cloud.py +++ b/tests/test_cloud.py @@ -5,6 +5,9 @@ from unittest.mock import patch from aiohttp import WSMessage, WSMsgType +from homeassistant import core +from homeassistant.components import switch +from homeassistant.const import Platform from homeassistant.setup import async_setup_component import pytest from pytest_homeassistant_custom_component.common import MockConfigEntry @@ -198,7 +201,10 @@ async def test_cloud_messages_invalid_format(hass_platform_cloud_connection, con mock_reconnect.assert_called_once() -async def test_cloud_req_user_devices(hass_platform_cloud_connection, config, aioclient_mock): +@pytest.mark.parametrize('expected_lingering_timers', [True]) +async def test_cloud_req_user_devices( + hass_platform_cloud_connection, config, aioclient_mock, expected_lingering_timers +): hass = hass_platform_cloud_connection requests = [{ 'request_id': 'req_user_devices', @@ -354,8 +360,13 @@ async def test_cloud_req_user_devices_action(hass_platform_cloud_connection, con }) }] - await async_setup_component(hass, 'switch', {'switch': {'platform': 'demo'}}) - await hass.async_block_till_done() + with patch( + 'homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM', + [Platform.SWITCH], + ): + await async_setup_component(hass, core.DOMAIN, {}) + await async_setup_component(hass, switch.DOMAIN, {switch.DOMAIN: {'platform': 'demo'}}) + await hass.async_block_till_done() assert hass.states.get('switch.ac').state == 'off' diff --git a/tests/test_entity.py b/tests/test_entity.py index bfc7a64b..0cf61d14 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -88,14 +88,15 @@ def supported(self) -> bool: async def test_yandex_entity_capabilities(hass): light = DemoLight( - unique_id='test_light', - name='Light', + 'test_light', + 'Light', available=True, state=True, ) light.hass = hass light.entity_id = 'light.test' - await light.async_update_ha_state() + light._attr_name = 'Light' + light.async_write_ha_state() state = hass.states.get('light.test') state_sensor = State('sensor.test', '33') diff --git a/tests/test_http.py b/tests/test_http.py index e6e0f596..568a34ed 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,6 +1,12 @@ from http import HTTPStatus +from unittest.mock import patch +from homeassistant import core +from homeassistant.components import switch +from homeassistant.const import Platform from homeassistant.setup import async_setup_component +import pytest +from pytest_homeassistant_custom_component.common import MockUser from custom_components.yandex_smart_home import async_unload_entry from custom_components.yandex_smart_home.http import ( @@ -22,7 +28,8 @@ async def test_unauthorized_view(hass_platform, aiohttp_client, config_entry, so assert response.status == HTTPStatus.NOT_FOUND -async def test_ping(hass_platform, aiohttp_client, config_entry, socket_enabled): +@pytest.mark.parametrize('expected_lingering_timers', [True]) +async def test_ping(hass_platform, aiohttp_client, config_entry, socket_enabled, expected_lingering_timers): http_client = await aiohttp_client(hass_platform.http.app) response = await http_client.get(YandexSmartHomePingView.url) assert response.status == HTTPStatus.OK @@ -58,7 +65,8 @@ async def test_user_unlink(hass_platform, hass_client): assert await response.json() == {'request_id': REQ_ID} -async def test_user_devices(hass_platform, hass_client, hass_admin_user): +@pytest.mark.parametrize('expected_lingering_timers', [True]) +async def test_user_devices(hass_platform, hass_client, hass_admin_user: MockUser, expected_lingering_timers): http_client = await hass_client() response = await http_client.get( YandexSmartHomeView.url + '/user/devices', @@ -167,10 +175,17 @@ async def test_user_devices_query(hass_platform, hass_client): async def test_user_devices_action(hass_platform, hass_client): - await async_setup_component(hass_platform, 'switch', {'switch': {'platform': 'demo'}}) - await hass_platform.async_block_till_done() + hass = hass_platform - assert hass_platform.states.get('switch.ac').state == 'off' + with patch( + 'homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM', + [Platform.SWITCH], + ): + await async_setup_component(hass, core.DOMAIN, {}) + await async_setup_component(hass, switch.DOMAIN, {switch.DOMAIN: {'platform': 'demo'}}) + await hass.async_block_till_done() + + assert hass.states.get('switch.ac').state == 'off' http_client = await hass_client() @@ -210,5 +225,5 @@ async def test_user_devices_action(hass_platform, hass_client): } } - await hass_platform.async_block_till_done() - assert hass_platform.states.get('switch.ac').state == 'on' + await hass.async_block_till_done() + assert hass.states.get('switch.ac').state == 'on' diff --git a/tests/test_init.py b/tests/test_init.py index 89652181..e2848cc8 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -117,7 +117,7 @@ async def test_valid_config(hass): ], 'data': { 'media_content_type': 'channel', - 'media_content_id': Template('{{ value }}') + 'media_content_id': Template('{{ value }}', hass) } }, 'increase_value': { @@ -295,7 +295,7 @@ async def test_valid_config(hass): 'set_value': { 'service': 'climate.set_temperature', 'data': { - 'temperature': Template('{{ value }}') + 'temperature': Template('{{ value }}', hass) }, 'target': { 'entity_id': [ @@ -538,7 +538,8 @@ async def test_async_remove_entry_cloud(hass, config_entry_cloud_connection, aio assert len(caplog.records) == 0 -async def test_async_setup_update_from_yaml(hass, hass_admin_user): +@pytest.mark.parametrize('expected_lingering_timers', [True]) +async def test_async_setup_update_from_yaml(hass, hass_admin_user, expected_lingering_timers): await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: {}}) entry = MockConfigEntry( @@ -622,6 +623,7 @@ async def test_async_setup_update_from_yaml_checksum(hass, hass_admin_user): assert await async_setup(hass, await async_integration_yaml_config(hass, DOMAIN)) with patch('custom_components.yandex_smart_home.async_setup', return_value=True): assert await async_setup_entry(hass, entry) + await hass.async_block_till_done() assert entry.options[const.CONF_PRESSURE_UNIT] == 'mmHg' - assert entry.data[const.YAML_CONFIG_HASH] == 'cbe26e947d35ed6222f97e493b32d94f' + assert entry.data[const.YAML_CONFIG_HASH] == '0eedd9f5ee18739bf910b36d2f9f1c6e' diff --git a/tests/test_prop.py b/tests/test_prop.py index 661c8e6e..12ce6fbb 100644 --- a/tests/test_prop.py +++ b/tests/test_prop.py @@ -1,6 +1,10 @@ from __future__ import annotations +from unittest.mock import patch + +from homeassistant import core from homeassistant.components import binary_sensor, climate, sensor +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -47,11 +51,16 @@ def assert_no_properties(hass: HomeAssistant, config: Config, state: State, async def test_property_demo_platform(hass): - for component in climate, sensor, binary_sensor: - await async_setup_component( - hass, component.DOMAIN, {component.DOMAIN: [{'platform': 'demo'}]} - ) - await hass.async_block_till_done() + with patch( + 'homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM', + [Platform.CLIMATE, Platform.SENSOR, Platform.BINARY_SENSOR], + ): + await async_setup_component(hass, core.DOMAIN, {}) + for component in climate, sensor, binary_sensor: + await async_setup_component( + hass, component.DOMAIN, {component.DOMAIN: [{'platform': 'demo'}]} + ) + await hass.async_block_till_done() # for x in hass.states.async_all(): # e = YandexEntity(hass, BASIC_CONFIG, x) diff --git a/tests/test_smart_home.py b/tests/test_smart_home.py index a284db00..3e594ce3 100644 --- a/tests/test_smart_home.py +++ b/tests/test_smart_home.py @@ -368,7 +368,6 @@ async def set_state(self, data, state): }] } - assert len(caplog.records) == 2 assert 'Invalid error code' in caplog.records[-1].message hass.states.async_set('sensor.foo', 'bar')