diff --git a/salusfy/__init__.py b/custom_components/salusfy/__init__.py similarity index 96% rename from salusfy/__init__.py rename to custom_components/salusfy/__init__.py index 6ff4bcd..fa79686 100644 --- a/salusfy/__init__.py +++ b/custom_components/salusfy/__init__.py @@ -1,7 +1,7 @@ -"""The Salus component.""" - -from .state import State -from .web_client import WebClient -from .thermostat_entity import ThermostatEntity -from .ha_temperature_client import HaTemperatureClient -from .client import Client +"""The Salus component.""" + +from .state import State +from .web_client import WebClient +from .thermostat_entity import ThermostatEntity +from .ha_temperature_client import HaTemperatureClient +from .client import Client diff --git a/salusfy/client.py b/custom_components/salusfy/client.py similarity index 96% rename from salusfy/client.py rename to custom_components/salusfy/client.py index 47d115c..6b1e822 100644 --- a/salusfy/client.py +++ b/custom_components/salusfy/client.py @@ -1,83 +1,83 @@ -""" -Client which wraps the web client but handles -the retrieval of current temperature by calling -a specialized client. -""" -import logging - -from homeassistant.components.climate.const import ( - HVACMode, - HVACAction, -) - -from . import ( - WebClient, - HaTemperatureClient, - State, -) - -_LOGGER = logging.getLogger(__name__) - - -class Client: - """Mocks requests to Salus web application""" - - def __init__( - self, - web_client: WebClient, - temperature_client: HaTemperatureClient): - """Initialize the client.""" - self._state = None - self._web_client = web_client - self._temperature_client = temperature_client - - async def set_temperature(self, temperature: float) -> None: - """Set new target temperature.""" - - _LOGGER.info("Delegating set_temperature to web client...") - - await self._web_client.set_temperature(temperature) - - self._state.target_temperature = temperature - - self.assume_hvac_action() - - async def set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set HVAC mode, via URL commands.""" - - _LOGGER.info("Delegating set_hvac_mode to web client...") - - await self._web_client.set_hvac_mode(hvac_mode) - - self._state.mode = hvac_mode - - self.assume_hvac_action() - - def assume_hvac_action(self) -> None: - """Assumes what the hvac action is based on - the mode and current/target temperatures""" - if self._state.mode == HVACMode.OFF: - _LOGGER.info("Assuming action is IDLE...") - self._state.action = HVACAction.IDLE - return - - if self._state.target_temperature > self._state.current_temperature: - _LOGGER.info( - "Assuming action is HEATING based on target temperature...") - self._state.action = HVACAction.HEATING - return - - _LOGGER.info("Assuming action is IDLE based on target temperature...") - self._state.action = HVACAction.IDLE - - async def get_state(self) -> State: - """Retrieves the status""" - - if self._state is None: - _LOGGER.info("Delegating get_state to web client...") - self._state = await self._web_client.get_state() - - _LOGGER.info("Updating current temperature from temperature client...") - self._state.current_temperature = await self._temperature_client.current_temperature() - - return self._state +""" +Client which wraps the web client but handles +the retrieval of current temperature by calling +a specialized client. +""" +import logging + +from homeassistant.components.climate.const import ( + HVACMode, + HVACAction, +) + +from . import ( + WebClient, + HaTemperatureClient, + State, +) + +_LOGGER = logging.getLogger(__name__) + + +class Client: + """Mocks requests to Salus web application""" + + def __init__( + self, + web_client: WebClient, + temperature_client: HaTemperatureClient): + """Initialize the client.""" + self._state = None + self._web_client = web_client + self._temperature_client = temperature_client + + async def set_temperature(self, temperature: float) -> None: + """Set new target temperature.""" + + _LOGGER.info("Delegating set_temperature to web client...") + + await self._web_client.set_temperature(temperature) + + self._state.target_temperature = temperature + + self.assume_hvac_action() + + async def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set HVAC mode, via URL commands.""" + + _LOGGER.info("Delegating set_hvac_mode to web client...") + + await self._web_client.set_hvac_mode(hvac_mode) + + self._state.mode = hvac_mode + + self.assume_hvac_action() + + def assume_hvac_action(self) -> None: + """Assumes what the hvac action is based on + the mode and current/target temperatures""" + if self._state.mode == HVACMode.OFF: + _LOGGER.info("Assuming action is IDLE...") + self._state.action = HVACAction.IDLE + return + + if self._state.target_temperature > self._state.current_temperature: + _LOGGER.info( + "Assuming action is HEATING based on target temperature...") + self._state.action = HVACAction.HEATING + return + + _LOGGER.info("Assuming action is IDLE based on target temperature...") + self._state.action = HVACAction.IDLE + + async def get_state(self) -> State: + """Retrieves the status""" + + if self._state is None: + _LOGGER.info("Delegating get_state to web client...") + self._state = await self._web_client.get_state() + + _LOGGER.info("Updating current temperature from temperature client...") + self._state.current_temperature = await self._temperature_client.current_temperature() + + return self._state diff --git a/salusfy/climate.py b/custom_components/salusfy/climate.py similarity index 96% rename from salusfy/climate.py rename to custom_components/salusfy/climate.py index 3fd3c6b..3db8ca4 100644 --- a/salusfy/climate.py +++ b/custom_components/salusfy/climate.py @@ -1,113 +1,113 @@ -""" -Adds support for the Salus Thermostat units. -""" -import logging -from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.components.climate import PLATFORM_SCHEMA -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - CONF_ID, - CONF_ENTITY_ID, - CONF_ACCESS_TOKEN, - CONF_HOST -) - -from . import simulator -from . import ( - ThermostatEntity, - Client, - WebClient, - HaTemperatureClient, -) - -CONF_SIMULATOR = 'simulator' -CONF_ENABLE_TEMPERATURE_CLIENT = 'enable_temperature_client' - - -__version__ = "0.3.0" - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Salus Thermostat" - -CONF_NAME = "name" - -DOMAIN = "salusfy" -PLATFORMS = ["climate"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_NAME, - default=DEFAULT_NAME): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_ID): cv.string, - vol.Optional( - CONF_SIMULATOR, - default=False): cv.boolean, - vol.Optional( - CONF_ENABLE_TEMPERATURE_CLIENT, - default=False): cv.boolean, - vol.Optional( - CONF_ENTITY_ID, - default=''): cv.string, - vol.Optional( - CONF_ACCESS_TOKEN, - default=''): cv.string, - vol.Optional( - CONF_HOST, - default='localhost'): cv.string}) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the E-Thermostat platform.""" - - _LOGGER.info("Discovery info: %s", discovery_info) - - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - - client = create_client_from(config) - - name = config.get(CONF_NAME) - async_add_entities( - [ThermostatEntity(name, client)], update_before_add=True - ) - - -def create_client_from(config) -> Client: - """Creates a client object based on the specified configuration""" - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - device_id = config.get(CONF_ID) - enable_simulator = config.get(CONF_SIMULATOR) - - if enable_simulator: - _LOGGER.info('Registering Salus Thermostat client simulator...') - - return Client(simulator.WebClient(), simulator.TemperatureClient()) - - web_client = WebClient(username, password, device_id) - - enable_temperature_client = config.get(CONF_ENABLE_TEMPERATURE_CLIENT) - - if not enable_temperature_client: - _LOGGER.info('Registering Salus Thermostat client...') - - return web_client - - entity_id = config.get(CONF_ENTITY_ID) - host = config.get(CONF_HOST) - access_token = config.get(CONF_ACCESS_TOKEN) - - _LOGGER.info( - 'Registering Salus Thermostat client with Temperature client...') - - ha_client = HaTemperatureClient(host, entity_id, access_token) - return Client(web_client, ha_client) +""" +Adds support for the Salus Thermostat units. +""" +import logging +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.components.climate import PLATFORM_SCHEMA +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + CONF_ID, + CONF_ENTITY_ID, + CONF_ACCESS_TOKEN, + CONF_HOST +) + +from . import simulator +from . import ( + ThermostatEntity, + Client, + WebClient, + HaTemperatureClient, +) + +CONF_SIMULATOR = 'simulator' +CONF_ENABLE_TEMPERATURE_CLIENT = 'enable_temperature_client' + + +__version__ = "0.3.0" + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Salus Thermostat" + +CONF_NAME = "name" + +DOMAIN = "salusfy" +PLATFORMS = ["climate"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional( + CONF_NAME, + default=DEFAULT_NAME): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_ID): cv.string, + vol.Optional( + CONF_SIMULATOR, + default=False): cv.boolean, + vol.Optional( + CONF_ENABLE_TEMPERATURE_CLIENT, + default=False): cv.boolean, + vol.Optional( + CONF_ENTITY_ID, + default=''): cv.string, + vol.Optional( + CONF_ACCESS_TOKEN, + default=''): cv.string, + vol.Optional( + CONF_HOST, + default='localhost'): cv.string}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the E-Thermostat platform.""" + + _LOGGER.info("Discovery info: %s", discovery_info) + + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + + client = create_client_from(config) + + name = config.get(CONF_NAME) + async_add_entities( + [ThermostatEntity(name, client)], update_before_add=True + ) + + +def create_client_from(config) -> Client: + """Creates a client object based on the specified configuration""" + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + device_id = config.get(CONF_ID) + enable_simulator = config.get(CONF_SIMULATOR) + + if enable_simulator: + _LOGGER.info('Registering Salus Thermostat client simulator...') + + return Client(simulator.WebClient(), simulator.TemperatureClient()) + + web_client = WebClient(username, password, device_id) + + enable_temperature_client = config.get(CONF_ENABLE_TEMPERATURE_CLIENT) + + if not enable_temperature_client: + _LOGGER.info('Registering Salus Thermostat client...') + + return web_client + + entity_id = config.get(CONF_ENTITY_ID) + host = config.get(CONF_HOST) + access_token = config.get(CONF_ACCESS_TOKEN) + + _LOGGER.info( + 'Registering Salus Thermostat client with Temperature client...') + + ha_client = HaTemperatureClient(host, entity_id, access_token) + return Client(web_client, ha_client) diff --git a/salusfy/ha_temperature_client.py b/custom_components/salusfy/ha_temperature_client.py similarity index 96% rename from salusfy/ha_temperature_client.py rename to custom_components/salusfy/ha_temperature_client.py index f97c685..37f1450 100644 --- a/salusfy/ha_temperature_client.py +++ b/custom_components/salusfy/ha_temperature_client.py @@ -1,40 +1,40 @@ -"""Reduces reliance on the Salus API""" -import aiohttp - -# pylint: disable=too-few-public-methods - - -class HaTemperatureClient: - """ - Retrieves the current temperature from - another entity from the Home Assistant API - """ - - def __init__(self, host, entity_id, access_token): - self._entity_id = entity_id - self._host = host - self._access_token = access_token - - async def current_temperature(self) -> float: - """Gets the current temperature from HA""" - - url = F"http://{self._host}:8123/api/states/{self._entity_id}" - - headers = { - "Authorization": F"Bearer {self._access_token}", - "Content-Type": "application/json", - } - - async with aiohttp.ClientSession() as session: - async with session.get(url, headers=headers) as response: - - body = await response.json() - - if 'state' not in body: - return None - - state = body['state'] - if state == 'unavailable': - return None - - return float(state) +"""Reduces reliance on the Salus API""" +import aiohttp + +# pylint: disable=too-few-public-methods + + +class HaTemperatureClient: + """ + Retrieves the current temperature from + another entity from the Home Assistant API + """ + + def __init__(self, host, entity_id, access_token): + self._entity_id = entity_id + self._host = host + self._access_token = access_token + + async def current_temperature(self) -> float: + """Gets the current temperature from HA""" + + url = F"http://{self._host}:8123/api/states/{self._entity_id}" + + headers = { + "Authorization": F"Bearer {self._access_token}", + "Content-Type": "application/json", + } + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + + body = await response.json() + + if 'state' not in body: + return None + + state = body['state'] + if state == 'unavailable': + return None + + return float(state) diff --git a/custom_components/salusfy/manifest.json b/custom_components/salusfy/manifest.json new file mode 100644 index 0000000..3cca9e6 --- /dev/null +++ b/custom_components/salusfy/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "salusfy", + "name": "Salus Thermostat", + "version": "0.7.0", + "documentation": "https://github.com/matthewturner/salusfy", + "issue_tracker": "https://github.com/matthewturner/salusfy/issues", + "requirements": [], + "dependencies": [], + "codeowners": [ + "@matthewturner" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/salusfy/services.yaml b/custom_components/salusfy/services.yaml similarity index 100% rename from salusfy/services.yaml rename to custom_components/salusfy/services.yaml diff --git a/salusfy/simulator/__init__.py b/custom_components/salusfy/simulator/__init__.py similarity index 97% rename from salusfy/simulator/__init__.py rename to custom_components/salusfy/simulator/__init__.py index 954253c..a08bfad 100644 --- a/salusfy/simulator/__init__.py +++ b/custom_components/salusfy/simulator/__init__.py @@ -1,2 +1,2 @@ -from .temperature_client import TemperatureClient -from .web_client import WebClient +from .temperature_client import TemperatureClient +from .web_client import WebClient diff --git a/salusfy/simulator/temperature_client.py b/custom_components/salusfy/simulator/temperature_client.py similarity index 94% rename from salusfy/simulator/temperature_client.py rename to custom_components/salusfy/simulator/temperature_client.py index 2ae6024..597de2c 100644 --- a/salusfy/simulator/temperature_client.py +++ b/custom_components/salusfy/simulator/temperature_client.py @@ -1,11 +1,11 @@ -""" -Adds support for simulating the Salus Thermostats. -""" - - -class TemperatureClient: - def __init__(self): - pass - - async def current_temperature(self) -> float: - return 15.9 +""" +Adds support for simulating the Salus Thermostats. +""" + + +class TemperatureClient: + def __init__(self): + pass + + async def current_temperature(self) -> float: + return 15.9 diff --git a/salusfy/simulator/web_client.py b/custom_components/salusfy/simulator/web_client.py similarity index 96% rename from salusfy/simulator/web_client.py rename to custom_components/salusfy/simulator/web_client.py index ea99558..3391979 100644 --- a/salusfy/simulator/web_client.py +++ b/custom_components/salusfy/simulator/web_client.py @@ -1,40 +1,40 @@ -""" -Adds support for simulating the Salus Thermostats. -""" -import logging - -from homeassistant.components.climate.const import HVACMode - -from ..state import State - - -_LOGGER = logging.getLogger(__name__) - - -class WebClient: - """Mocks requests to Salus web application""" - - def __init__(self): - """Initialize the client.""" - self._state = State() - self._state.target_temperature = 20.1 - self._state.current_temperature = 15.1 - self._state.frost = 10.1 - - async def set_temperature(self, temperature: float) -> None: - """Set new target temperature.""" - - _LOGGER.info("Setting temperature to %.1f...", temperature) - - self._state.target_temperature = temperature - - async def set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set HVAC mode, via URL commands.""" - - _LOGGER.info("Setting the HVAC mode to %s...", hvac_mode) - - self._state.mode = hvac_mode - - async def get_state(self) -> State: - """Retrieves the mock status""" - return self._state +""" +Adds support for simulating the Salus Thermostats. +""" +import logging + +from homeassistant.components.climate.const import HVACMode + +from ..state import State + + +_LOGGER = logging.getLogger(__name__) + + +class WebClient: + """Mocks requests to Salus web application""" + + def __init__(self): + """Initialize the client.""" + self._state = State() + self._state.target_temperature = 20.1 + self._state.current_temperature = 15.1 + self._state.frost = 10.1 + + async def set_temperature(self, temperature: float) -> None: + """Set new target temperature.""" + + _LOGGER.info("Setting temperature to %.1f...", temperature) + + self._state.target_temperature = temperature + + async def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set HVAC mode, via URL commands.""" + + _LOGGER.info("Setting the HVAC mode to %s...", hvac_mode) + + self._state.mode = hvac_mode + + async def get_state(self) -> State: + """Retrieves the mock status""" + return self._state diff --git a/salusfy/state.py b/custom_components/salusfy/state.py similarity index 95% rename from salusfy/state.py rename to custom_components/salusfy/state.py index f2924de..d0a7db4 100644 --- a/salusfy/state.py +++ b/custom_components/salusfy/state.py @@ -1,13 +1,13 @@ -"""Exposes state of the thermostat.""" - -import dataclasses - - -@dataclasses.dataclass -class State: - """The state of the thermostat.""" - current_temperature = None - target_temperature = None - frost = None - action = None - mode = None +"""Exposes state of the thermostat.""" + +import dataclasses + + +@dataclasses.dataclass +class State: + """The state of the thermostat.""" + current_temperature = None + target_temperature = None + frost = None + action = None + mode = None diff --git a/salusfy/thermostat_entity.py b/custom_components/salusfy/thermostat_entity.py similarity index 96% rename from salusfy/thermostat_entity.py rename to custom_components/salusfy/thermostat_entity.py index b2cdac9..019ff34 100644 --- a/salusfy/thermostat_entity.py +++ b/custom_components/salusfy/thermostat_entity.py @@ -1,136 +1,136 @@ -from homeassistant.components.climate.const import ( - HVACAction, - HVACMode, - ClimateEntityFeature, - PRESET_NONE, -) - -from homeassistant.const import ( - ATTR_TEMPERATURE, - UnitOfTemperature, -) - -from .web_client import ( - MAX_TEMP, - MIN_TEMP -) - -try: - from homeassistant.components.climate import ClimateEntity -except ImportError: - from homeassistant.components.climate import ClimateDevice as ClimateEntity - - -class ThermostatEntity(ClimateEntity): - """Representation of a Salus Thermostat device.""" - - def __init__(self, name, client): - """Initialize the thermostat.""" - self._name = name - self._client = client - - self._state = None - - self._enable_turn_on_off_backwards_compatibility = False - - @property - def supported_features(self) -> ClimateEntityFeature: - """Return the list of supported features.""" - return (ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TURN_ON - | ClimateEntityFeature.TURN_OFF) - - @property - def name(self) -> str: - """Return the name of the thermostat.""" - return self._name - - @property - def unique_id(self) -> str: - """Return the unique ID for this thermostat.""" - return "_".join([self._name, "climate"]) - - @property - def should_poll(self) -> bool: - """Return if polling is required.""" - return True - - @property - def min_temp(self) -> float: - """Return the minimum temperature.""" - return MIN_TEMP - - @property - def max_temp(self) -> float: - """Return the maximum temperature.""" - return MAX_TEMP - - @property - def temperature_unit(self) -> UnitOfTemperature: - """Return the unit of measurement.""" - return UnitOfTemperature.CELSIUS - - @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self._state.current_temperature - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._state.target_temperature - - @property - def hvac_mode(self) -> HVACMode: - """Return hvac operation ie. heat, cool mode.""" - - return self._state.mode - - @property - def hvac_modes(self) -> list[HVACMode]: - """HVAC modes.""" - return [HVACMode.HEAT, HVACMode.OFF] - - @property - def hvac_action(self) -> HVACAction: - """Return the current running hvac operation.""" - return self._state.action - - @property - def preset_mode(self) -> str: - """Return the current preset mode, e.g., home, away, temp.""" - return PRESET_NONE - - @property - def preset_modes(self) -> list[str]: - """Return a list of available preset modes.""" - return [PRESET_NONE] - - async def async_set_temperature(self, **kwargs) -> None: - """Set new target temperature.""" - - temperature = kwargs.get(ATTR_TEMPERATURE) - - if temperature is None: - return - - await self._client.set_temperature(temperature) - - self._state.target_temperature = temperature - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set HVAC mode, via URL commands.""" - - await self._client.set_hvac_mode(hvac_mode) - - self._state.mode = hvac_mode - - async def async_turn_off(self) -> None: - await self.async_set_hvac_mode(HVACMode.OFF) - - async def async_turn_on(self) -> None: - await self.async_set_hvac_mode(HVACMode.HEAT) - - async def async_update(self) -> None: - """Retrieve latest state data.""" - self._state = await self._client.get_state() +from homeassistant.components.climate.const import ( + HVACAction, + HVACMode, + ClimateEntityFeature, + PRESET_NONE, +) + +from homeassistant.const import ( + ATTR_TEMPERATURE, + UnitOfTemperature, +) + +from .web_client import ( + MAX_TEMP, + MIN_TEMP +) + +try: + from homeassistant.components.climate import ClimateEntity +except ImportError: + from homeassistant.components.climate import ClimateDevice as ClimateEntity + + +class ThermostatEntity(ClimateEntity): + """Representation of a Salus Thermostat device.""" + + def __init__(self, name, client): + """Initialize the thermostat.""" + self._name = name + self._client = client + + self._state = None + + self._enable_turn_on_off_backwards_compatibility = False + + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + return (ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF) + + @property + def name(self) -> str: + """Return the name of the thermostat.""" + return self._name + + @property + def unique_id(self) -> str: + """Return the unique ID for this thermostat.""" + return "_".join([self._name, "climate"]) + + @property + def should_poll(self) -> bool: + """Return if polling is required.""" + return True + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return MIN_TEMP + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return MAX_TEMP + + @property + def temperature_unit(self) -> UnitOfTemperature: + """Return the unit of measurement.""" + return UnitOfTemperature.CELSIUS + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return self._state.current_temperature + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._state.target_temperature + + @property + def hvac_mode(self) -> HVACMode: + """Return hvac operation ie. heat, cool mode.""" + + return self._state.mode + + @property + def hvac_modes(self) -> list[HVACMode]: + """HVAC modes.""" + return [HVACMode.HEAT, HVACMode.OFF] + + @property + def hvac_action(self) -> HVACAction: + """Return the current running hvac operation.""" + return self._state.action + + @property + def preset_mode(self) -> str: + """Return the current preset mode, e.g., home, away, temp.""" + return PRESET_NONE + + @property + def preset_modes(self) -> list[str]: + """Return a list of available preset modes.""" + return [PRESET_NONE] + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + + temperature = kwargs.get(ATTR_TEMPERATURE) + + if temperature is None: + return + + await self._client.set_temperature(temperature) + + self._state.target_temperature = temperature + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set HVAC mode, via URL commands.""" + + await self._client.set_hvac_mode(hvac_mode) + + self._state.mode = hvac_mode + + async def async_turn_off(self) -> None: + await self.async_set_hvac_mode(HVACMode.OFF) + + async def async_turn_on(self) -> None: + await self.async_set_hvac_mode(HVACMode.HEAT) + + async def async_update(self) -> None: + """Retrieve latest state data.""" + self._state = await self._client.get_state() diff --git a/salusfy/web_client.py b/custom_components/salusfy/web_client.py similarity index 97% rename from salusfy/web_client.py rename to custom_components/salusfy/web_client.py index fbc7270..8835efa 100644 --- a/salusfy/web_client.py +++ b/custom_components/salusfy/web_client.py @@ -1,192 +1,192 @@ -""" -Adds support for the Salus Thermostat units. -""" -import time -import logging -import re -import json -import aiohttp - -from homeassistant.components.climate.const import ( - HVACMode, - HVACAction, -) - -from .state import State - -_LOGGER = logging.getLogger(__name__) - -URL_LOGIN = "https://salus-it500.com/public/login.php" -URL_GET_TOKEN = "https://salus-it500.com/public/control.php" -URL_GET_DATA = "https://salus-it500.com/public/ajax_device_values.php" -URL_SET_DATA = "https://salus-it500.com/includes/set.php" - -# Values from web interface -MIN_TEMP = 5 -MAX_TEMP = 34.5 -MAX_TOKEN_AGE_SECONDS = 60 * 10 - - -class WebClient: - """Adapter around Salus IT500 web application.""" - - def __init__(self, username: str, password: str, device_id: str): - """Initialize the client.""" - self._username = username - self._password = password - self._id = device_id - self._token = None - self._token_retrieved_at = None - - async def set_temperature(self, temperature: float) -> None: - """Set new target temperature, via URL commands.""" - - _LOGGER.info("Setting the temperature to %.1f...", temperature) - - async with aiohttp.ClientSession() as session: - token = await self.obtain_token(session) - - payload = { - "token": token, - "devId": self._id, - "tempUnit": "0", - "current_tempZ1_set": "1", - "current_tempZ1": temperature} - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - try: - await session.post(URL_SET_DATA, data=payload, headers=headers) - _LOGGER.info("Salusfy set_temperature: OK") - except BaseException: - _LOGGER.error("Error Setting the temperature.") - - async def set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set HVAC mode, via URL commands.""" - - _LOGGER.info("Setting the HVAC mode to %s...", hvac_mode) - - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - auto = "1" - if hvac_mode == HVACMode.OFF: - auto = "1" - elif hvac_mode == HVACMode.HEAT: - auto = "0" - - async with aiohttp.ClientSession() as session: - token = await self.obtain_token(session) - - payload = { - "token": token, - "devId": self._id, - "auto": auto, - "auto_setZ1": "1"} - try: - await session.post(URL_SET_DATA, data=payload, headers=headers) - except BaseException: - _LOGGER.error("Error Setting HVAC mode to %s", hvac_mode) - - async def obtain_token(self, session: str) -> str: - """Gets the existing session token of the thermostat or retrieves a new one if expired.""" - - if self._token is None: - _LOGGER.info("Retrieving token for the first time this session...") - await self.get_token(session) - return self._token - - if self._token_retrieved_at > time.time() - MAX_TOKEN_AGE_SECONDS: - _LOGGER.info("Using cached token...") - return self._token - - _LOGGER.info("Token has expired, getting new one...") - await self.get_token(session) - return self._token - - async def get_token(self, session: str) -> None: - """Get the Session Token of the Thermostat.""" - - _LOGGER.info("Getting token from Salus...") - - payload = { - "IDemail": self._username, - "password": self._password, - "login": "Login", - "keep_logged_in": "1"} - headers = {"Content-Type": "application/x-www-form-urlencoded"} - - try: - await session.post(URL_LOGIN, data=payload, headers=headers) - params = {"devId": self._id} - token_response = await session.get(URL_GET_TOKEN, params=params) - body = await token_response.text() - result = re.search( - '', body) - _LOGGER.info("Salusfy get_token OK") - self._token = result.group(1) - self._token_retrieved_at = time.time() - except Exception as e: - self._token = None - self._token_retrieved_at = None - _LOGGER.error("Error getting the session token.") - _LOGGER.error(e) - - async def get_state(self) -> State: - """Retrieve the current state from the Salus gateway""" - - _LOGGER.info("Retrieving current state from Salus Gateway...") - - data = await self.get_state_data() - - return WebClient.convert_to_state(data) - - @classmethod - def convert_to_state(cls, data: dict) -> State: - """Converts the data payload to a state object""" - state = State() - state.target_temperature = float(data["CH1currentSetPoint"]) - state.current_temperature = float(data["CH1currentRoomTemp"]) - state.frost = float(data["frost"]) - - status = data['CH1heatOnOffStatus'] - if status == "1": - state.action = HVACAction.HEATING - else: - state.action = HVACAction.IDLE - - heat_on_off = data['CH1heatOnOff'] - auto_mode = data['CH1autoMode'] - if heat_on_off == "0" and auto_mode == "0": - state.mode = HVACMode.AUTO - else: - if heat_on_off == "1": - state.mode = HVACMode.OFF - else: - state.mode = HVACMode.HEAT - - return state - - async def get_state_data(self) -> dict: - """Retrieves the raw state from the Salus gateway""" - - _LOGGER.info("Retrieving raw state from Salus Gateway...") - - async with aiohttp.ClientSession() as session: - token = await self.obtain_token(session) - - params = {"devId": self._id, "token": token, - "&_": str(int(round(time.time() * 1000)))} - try: - r = await session.get(url=URL_GET_DATA, params=params) - if not r: - _LOGGER.error("Could not get data from Salus.") - return None - except BaseException: - _LOGGER.error( - "Error Getting the data from Salus. Check the connection to salus-it500.com.") - return None - - body = await r.text() - _LOGGER.info("Salusfy get_data output %s", body) - data = json.loads(body) - - return data +""" +Adds support for the Salus Thermostat units. +""" +import time +import logging +import re +import json +import aiohttp + +from homeassistant.components.climate.const import ( + HVACMode, + HVACAction, +) + +from .state import State + +_LOGGER = logging.getLogger(__name__) + +URL_LOGIN = "https://salus-it500.com/public/login.php" +URL_GET_TOKEN = "https://salus-it500.com/public/control.php" +URL_GET_DATA = "https://salus-it500.com/public/ajax_device_values.php" +URL_SET_DATA = "https://salus-it500.com/includes/set.php" + +# Values from web interface +MIN_TEMP = 5 +MAX_TEMP = 34.5 +MAX_TOKEN_AGE_SECONDS = 60 * 10 + + +class WebClient: + """Adapter around Salus IT500 web application.""" + + def __init__(self, username: str, password: str, device_id: str): + """Initialize the client.""" + self._username = username + self._password = password + self._id = device_id + self._token = None + self._token_retrieved_at = None + + async def set_temperature(self, temperature: float) -> None: + """Set new target temperature, via URL commands.""" + + _LOGGER.info("Setting the temperature to %.1f...", temperature) + + async with aiohttp.ClientSession() as session: + token = await self.obtain_token(session) + + payload = { + "token": token, + "devId": self._id, + "tempUnit": "0", + "current_tempZ1_set": "1", + "current_tempZ1": temperature} + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + try: + await session.post(URL_SET_DATA, data=payload, headers=headers) + _LOGGER.info("Salusfy set_temperature: OK") + except BaseException: + _LOGGER.error("Error Setting the temperature.") + + async def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set HVAC mode, via URL commands.""" + + _LOGGER.info("Setting the HVAC mode to %s...", hvac_mode) + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + auto = "1" + if hvac_mode == HVACMode.OFF: + auto = "1" + elif hvac_mode == HVACMode.HEAT: + auto = "0" + + async with aiohttp.ClientSession() as session: + token = await self.obtain_token(session) + + payload = { + "token": token, + "devId": self._id, + "auto": auto, + "auto_setZ1": "1"} + try: + await session.post(URL_SET_DATA, data=payload, headers=headers) + except BaseException: + _LOGGER.error("Error Setting HVAC mode to %s", hvac_mode) + + async def obtain_token(self, session: str) -> str: + """Gets the existing session token of the thermostat or retrieves a new one if expired.""" + + if self._token is None: + _LOGGER.info("Retrieving token for the first time this session...") + await self.get_token(session) + return self._token + + if self._token_retrieved_at > time.time() - MAX_TOKEN_AGE_SECONDS: + _LOGGER.info("Using cached token...") + return self._token + + _LOGGER.info("Token has expired, getting new one...") + await self.get_token(session) + return self._token + + async def get_token(self, session: str) -> None: + """Get the Session Token of the Thermostat.""" + + _LOGGER.info("Getting token from Salus...") + + payload = { + "IDemail": self._username, + "password": self._password, + "login": "Login", + "keep_logged_in": "1"} + headers = {"Content-Type": "application/x-www-form-urlencoded"} + + try: + await session.post(URL_LOGIN, data=payload, headers=headers) + params = {"devId": self._id} + token_response = await session.get(URL_GET_TOKEN, params=params) + body = await token_response.text() + result = re.search( + '', body) + _LOGGER.info("Salusfy get_token OK") + self._token = result.group(1) + self._token_retrieved_at = time.time() + except Exception as e: + self._token = None + self._token_retrieved_at = None + _LOGGER.error("Error getting the session token.") + _LOGGER.error(e) + + async def get_state(self) -> State: + """Retrieve the current state from the Salus gateway""" + + _LOGGER.info("Retrieving current state from Salus Gateway...") + + data = await self.get_state_data() + + return WebClient.convert_to_state(data) + + @classmethod + def convert_to_state(cls, data: dict) -> State: + """Converts the data payload to a state object""" + state = State() + state.target_temperature = float(data["CH1currentSetPoint"]) + state.current_temperature = float(data["CH1currentRoomTemp"]) + state.frost = float(data["frost"]) + + status = data['CH1heatOnOffStatus'] + if status == "1": + state.action = HVACAction.HEATING + else: + state.action = HVACAction.IDLE + + heat_on_off = data['CH1heatOnOff'] + auto_mode = data['CH1autoMode'] + if heat_on_off == "0" and auto_mode == "0": + state.mode = HVACMode.AUTO + else: + if heat_on_off == "1": + state.mode = HVACMode.OFF + else: + state.mode = HVACMode.HEAT + + return state + + async def get_state_data(self) -> dict: + """Retrieves the raw state from the Salus gateway""" + + _LOGGER.info("Retrieving raw state from Salus Gateway...") + + async with aiohttp.ClientSession() as session: + token = await self.obtain_token(session) + + params = {"devId": self._id, "token": token, + "&_": str(int(round(time.time() * 1000)))} + try: + r = await session.get(url=URL_GET_DATA, params=params) + if not r: + _LOGGER.error("Could not get data from Salus.") + return None + except BaseException: + _LOGGER.error( + "Error Getting the data from Salus. Check the connection to salus-it500.com.") + return None + + body = await r.text() + _LOGGER.info("Salusfy get_data output %s", body) + data = json.loads(body) + + return data diff --git a/tests/__init__.py b/custom_components/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to custom_components/tests/__init__.py diff --git a/tests/config_adapter.py b/custom_components/tests/config_adapter.py similarity index 96% rename from tests/config_adapter.py rename to custom_components/tests/config_adapter.py index 5c30d22..dffaafc 100644 --- a/tests/config_adapter.py +++ b/custom_components/tests/config_adapter.py @@ -1,44 +1,44 @@ -# pylint: disable=too-few-public-methods -# pylint: disable=too-many-return-statements - -class ConfigAdapter: - """Simulates how Home Assistant loads configuration""" - - def __init__(self, config): - self._config = config - - def get(self, key: str) -> any: - """Returns the config value based on the Home Assistant key""" - - if key == 'name': - return 'Simulator' - - if key == 'id': - return self._config.DEVICE_ID - - if key == 'username': - return self._config.USERNAME - - if key == 'password': - return self._config.PASSWORD - - if key == 'simulator': - if hasattr(self._config, 'SIMULATOR'): - return self._config.SIMULATOR - return False - - if key == 'enable_temperature_client': - if hasattr(self._config, 'ENABLE_TEMPERATURE_CLIENT'): - return self._config.ENABLE_TEMPERATURE_CLIENT - return False - - if key == 'host': - return self._config.HOST - - if key == 'entity_id': - return self._config.ENTITY_ID - - if key == 'access_token': - return self._config.ACCESS_TOKEN - - return 'Unknown' +# pylint: disable=too-few-public-methods +# pylint: disable=too-many-return-statements + +class ConfigAdapter: + """Simulates how Home Assistant loads configuration""" + + def __init__(self, config): + self._config = config + + def get(self, key: str) -> any: + """Returns the config value based on the Home Assistant key""" + + if key == 'name': + return 'Simulator' + + if key == 'id': + return self._config.DEVICE_ID + + if key == 'username': + return self._config.USERNAME + + if key == 'password': + return self._config.PASSWORD + + if key == 'simulator': + if hasattr(self._config, 'SIMULATOR'): + return self._config.SIMULATOR + return False + + if key == 'enable_temperature_client': + if hasattr(self._config, 'ENABLE_TEMPERATURE_CLIENT'): + return self._config.ENABLE_TEMPERATURE_CLIENT + return False + + if key == 'host': + return self._config.HOST + + if key == 'entity_id': + return self._config.ENTITY_ID + + if key == 'access_token': + return self._config.ACCESS_TOKEN + + return 'Unknown' diff --git a/tests/entity_registry.py b/custom_components/tests/entity_registry.py similarity index 96% rename from tests/entity_registry.py rename to custom_components/tests/entity_registry.py index 33539ca..70d33a1 100644 --- a/tests/entity_registry.py +++ b/custom_components/tests/entity_registry.py @@ -1,29 +1,29 @@ -class EntityRegistry: - """Registry used for local and test executions.""" - - def __init__(self): - self._entities = [] - self._update_before_add = False - - def register(self, entities, **kwargs): - """Registers the list of entities.""" - self._update_before_add = kwargs.get('update_before_add') - self._entities.extend(entities) - - @property - def entities(self): - """Returns the list of entries registered during configuration.""" - return self._entities - - @property - def first(self): - """Returns the first entity registered.""" - return self._entities[0] - - @property - def update_before_add(self): - """ - Determines whether the update_before_add value - has been set during configuration. - """ - return self._update_before_add +class EntityRegistry: + """Registry used for local and test executions.""" + + def __init__(self): + self._entities = [] + self._update_before_add = False + + def register(self, entities, **kwargs): + """Registers the list of entities.""" + self._update_before_add = kwargs.get('update_before_add') + self._entities.extend(entities) + + @property + def entities(self): + """Returns the list of entries registered during configuration.""" + return self._entities + + @property + def first(self): + """Returns the first entity registered.""" + return self._entities[0] + + @property + def update_before_add(self): + """ + Determines whether the update_before_add value + has been set during configuration. + """ + return self._update_before_add diff --git a/tests/mock_config.py b/custom_components/tests/mock_config.py similarity index 96% rename from tests/mock_config.py rename to custom_components/tests/mock_config.py index 3c2a44d..502b01f 100644 --- a/tests/mock_config.py +++ b/custom_components/tests/mock_config.py @@ -1,8 +1,8 @@ -USERNAME = "john@smith.com" -PASSWORD = "12345" -DEVICE_ID = "999999" -ENTITY_ID = "sensor.everything_presence_one_temperature" -ACCESS_TOKEN = "some-secret" -SIMULATOR = True -ENABLE_TEMPERATURE_CLIENT = True -HOST = "192.168.0.99" +USERNAME = "john@smith.com" +PASSWORD = "12345" +DEVICE_ID = "999999" +ENTITY_ID = "sensor.everything_presence_one_temperature" +ACCESS_TOKEN = "some-secret" +SIMULATOR = True +ENABLE_TEMPERATURE_CLIENT = True +HOST = "192.168.0.99" diff --git a/tests/test_client.py b/custom_components/tests/test_client.py similarity index 96% rename from tests/test_client.py rename to custom_components/tests/test_client.py index e48fe56..8f1b46b 100644 --- a/tests/test_client.py +++ b/custom_components/tests/test_client.py @@ -1,175 +1,175 @@ -from unittest.mock import Mock -import pytest - -from homeassistant.components.climate.const import ( - HVACMode, - HVACAction -) - -from salusfy import (Client, State, WebClient, HaTemperatureClient) - -# pylint: disable=missing-function-docstring - - -@pytest.fixture(name="mock_client") -def mock_client_fixture(): - state = State() - state.current_temperature = 15.3 - state.target_temperature = 33.3 - - mock = Mock(WebClient) - mock.get_state.return_value = state - - return mock - - -@pytest.fixture(name="mock_ha_client") -def mock_ha_client_fixture(): - mock = Mock(HaTemperatureClient) - - mock.current_temperature.return_value = 21.1 - - return mock - - -@pytest.mark.asyncio -async def test_client_returns_target_temp_from_web_client(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - actual = await target.get_state() - - assert actual.target_temperature == 33.3 - - -@pytest.mark.asyncio -async def test_client_returns_target_temp_from_home_assistant_client(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - actual = await target.get_state() - - assert actual.current_temperature == 21.1 - - -@pytest.mark.asyncio -async def test_client_call_salus_client_only_once(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - await target.get_state() - await target.get_state() - - mock_client.get_state.assert_called_once() - - actual = await target.get_state() - assert actual.target_temperature == 33.3 - - -@pytest.mark.asyncio -async def test_client_delegates_set_temperature_salus_client(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_temperature(temperature=29.9) - - mock_client.set_temperature.assert_called_once_with(29.9) - - -@pytest.mark.asyncio -async def test_client_delegates_set_hvac_mode_to_salus_client(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) - - mock_client.set_hvac_mode.assert_called_once_with(HVACMode.HEAT) - - -@pytest.mark.asyncio -async def test_client_assumes_hvac_action_as_idle_when_mode_is_off(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_hvac_mode(hvac_mode=HVACMode.OFF) - - actual = await target.get_state() - - assert actual.action == HVACAction.IDLE - - -@pytest.mark.asyncio -async def test_client_sets_hvac_mode(mock_client, mock_ha_client): - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_hvac_mode(hvac_mode=HVACMode.OFF) - - actual = await target.get_state() - - assert actual.mode == HVACMode.OFF - - -@pytest.mark.asyncio -async def test_client_assumes_hvac_action_as_heat_when_mode_is_heat_and_target_temp_is_high( - mock_client, mock_ha_client): - - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_temperature(temperature=30) - await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) - - actual = await target.get_state() - - assert actual.action == HVACAction.HEATING - - -@pytest.mark.asyncio -async def test_client_assumes_hvac_action_as_idle_when_mode_is_heat_and_target_temp_is_low( - mock_client, mock_ha_client): - - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_temperature(temperature=4) - await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) - - actual = await target.get_state() - - assert actual.action == HVACAction.IDLE - - -@pytest.mark.asyncio -async def test_client_assumes_hvac_action_as_heat_when_mode_is_heat_and_target_temp_is_set_high( - mock_client, mock_ha_client): - - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) - await target.set_temperature(temperature=33) - - actual = await target.get_state() - - assert actual.action == HVACAction.HEATING - - -@pytest.mark.asyncio -async def test_client_assumes_hvac_action_as_idle_when_mode_is_heat_and_target_temp_is_set_low( - mock_client, mock_ha_client): - - target = Client(mock_client, mock_ha_client) - - await target.get_state() - - await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) - await target.set_temperature(temperature=4) - - actual = await target.get_state() - - assert actual.action == HVACAction.IDLE +from unittest.mock import Mock +import pytest + +from homeassistant.components.climate.const import ( + HVACMode, + HVACAction +) + +from salusfy import (Client, State, WebClient, HaTemperatureClient) + +# pylint: disable=missing-function-docstring + + +@pytest.fixture(name="mock_client") +def mock_client_fixture(): + state = State() + state.current_temperature = 15.3 + state.target_temperature = 33.3 + + mock = Mock(WebClient) + mock.get_state.return_value = state + + return mock + + +@pytest.fixture(name="mock_ha_client") +def mock_ha_client_fixture(): + mock = Mock(HaTemperatureClient) + + mock.current_temperature.return_value = 21.1 + + return mock + + +@pytest.mark.asyncio +async def test_client_returns_target_temp_from_web_client(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + actual = await target.get_state() + + assert actual.target_temperature == 33.3 + + +@pytest.mark.asyncio +async def test_client_returns_target_temp_from_home_assistant_client(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + actual = await target.get_state() + + assert actual.current_temperature == 21.1 + + +@pytest.mark.asyncio +async def test_client_call_salus_client_only_once(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + await target.get_state() + await target.get_state() + + mock_client.get_state.assert_called_once() + + actual = await target.get_state() + assert actual.target_temperature == 33.3 + + +@pytest.mark.asyncio +async def test_client_delegates_set_temperature_salus_client(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_temperature(temperature=29.9) + + mock_client.set_temperature.assert_called_once_with(29.9) + + +@pytest.mark.asyncio +async def test_client_delegates_set_hvac_mode_to_salus_client(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) + + mock_client.set_hvac_mode.assert_called_once_with(HVACMode.HEAT) + + +@pytest.mark.asyncio +async def test_client_assumes_hvac_action_as_idle_when_mode_is_off(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_hvac_mode(hvac_mode=HVACMode.OFF) + + actual = await target.get_state() + + assert actual.action == HVACAction.IDLE + + +@pytest.mark.asyncio +async def test_client_sets_hvac_mode(mock_client, mock_ha_client): + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_hvac_mode(hvac_mode=HVACMode.OFF) + + actual = await target.get_state() + + assert actual.mode == HVACMode.OFF + + +@pytest.mark.asyncio +async def test_client_assumes_hvac_action_as_heat_when_mode_is_heat_and_target_temp_is_high( + mock_client, mock_ha_client): + + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_temperature(temperature=30) + await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) + + actual = await target.get_state() + + assert actual.action == HVACAction.HEATING + + +@pytest.mark.asyncio +async def test_client_assumes_hvac_action_as_idle_when_mode_is_heat_and_target_temp_is_low( + mock_client, mock_ha_client): + + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_temperature(temperature=4) + await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) + + actual = await target.get_state() + + assert actual.action == HVACAction.IDLE + + +@pytest.mark.asyncio +async def test_client_assumes_hvac_action_as_heat_when_mode_is_heat_and_target_temp_is_set_high( + mock_client, mock_ha_client): + + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) + await target.set_temperature(temperature=33) + + actual = await target.get_state() + + assert actual.action == HVACAction.HEATING + + +@pytest.mark.asyncio +async def test_client_assumes_hvac_action_as_idle_when_mode_is_heat_and_target_temp_is_set_low( + mock_client, mock_ha_client): + + target = Client(mock_client, mock_ha_client) + + await target.get_state() + + await target.set_hvac_mode(hvac_mode=HVACMode.HEAT) + await target.set_temperature(temperature=4) + + actual = await target.get_state() + + assert actual.action == HVACAction.IDLE diff --git a/tests/test_climate.py b/custom_components/tests/test_climate.py similarity index 96% rename from tests/test_climate.py rename to custom_components/tests/test_climate.py index c21be84..863923f 100644 --- a/tests/test_climate.py +++ b/custom_components/tests/test_climate.py @@ -1,72 +1,72 @@ -import pytest - -from salusfy import climate -from .config_adapter import ConfigAdapter -from .entity_registry import EntityRegistry - -from . import mock_config - -# pylint: disable=missing-function-docstring - - -class MockHass: - """Mocks the HASS for use during unit tests.""" - @property - def services(self): - return self - - def has_service(self, - domain, # pylint: disable=unused-argument - service, # pylint: disable=unused-argument - ): - return False - - def async_register(self, domain, service, admin_handler, schema): - pass - - -@pytest.mark.asyncio -async def setup_climate_platform(): - registry = EntityRegistry() - config_adapter = ConfigAdapter(mock_config) - await climate.async_setup_platform(MockHass(), - config_adapter, - async_add_entities=registry.register, - discovery_info=None) - return registry - - -@pytest.mark.asyncio -async def test_entity_is_registered(): - registry = await setup_climate_platform() - - assert len(registry.entities) == 1 - - -@pytest.mark.asyncio -async def test_entity_is_updated_before_added(): - registry = await setup_climate_platform() - - assert registry.update_before_add - - -@pytest.mark.asyncio -async def test_entity_returns_mock_temperature(): - registry = await setup_climate_platform() - - thermostat = registry.first - - await thermostat.async_update() - - assert thermostat.current_temperature == 15.9 - - -@pytest.mark.asyncio -async def test_entity_returns_mock_target_temperature(): - registry = await setup_climate_platform() - - thermostat = registry.first - - await thermostat.async_update() - - assert thermostat.target_temperature == 20.1 +import pytest + +from salusfy import climate +from .config_adapter import ConfigAdapter +from .entity_registry import EntityRegistry + +from . import mock_config + +# pylint: disable=missing-function-docstring + + +class MockHass: + """Mocks the HASS for use during unit tests.""" + @property + def services(self): + return self + + def has_service(self, + domain, # pylint: disable=unused-argument + service, # pylint: disable=unused-argument + ): + return False + + def async_register(self, domain, service, admin_handler, schema): + pass + + +@pytest.mark.asyncio +async def setup_climate_platform(): + registry = EntityRegistry() + config_adapter = ConfigAdapter(mock_config) + await climate.async_setup_platform(MockHass(), + config_adapter, + async_add_entities=registry.register, + discovery_info=None) + return registry + + +@pytest.mark.asyncio +async def test_entity_is_registered(): + registry = await setup_climate_platform() + + assert len(registry.entities) == 1 + + +@pytest.mark.asyncio +async def test_entity_is_updated_before_added(): + registry = await setup_climate_platform() + + assert registry.update_before_add + + +@pytest.mark.asyncio +async def test_entity_returns_mock_temperature(): + registry = await setup_climate_platform() + + thermostat = registry.first + + await thermostat.async_update() + + assert thermostat.current_temperature == 15.9 + + +@pytest.mark.asyncio +async def test_entity_returns_mock_target_temperature(): + registry = await setup_climate_platform() + + thermostat = registry.first + + await thermostat.async_update() + + assert thermostat.target_temperature == 20.1 diff --git a/tests/test_thermostat_entity.py b/custom_components/tests/test_thermostat_entity.py similarity index 96% rename from tests/test_thermostat_entity.py rename to custom_components/tests/test_thermostat_entity.py index 787dba5..b2463fc 100644 --- a/tests/test_thermostat_entity.py +++ b/custom_components/tests/test_thermostat_entity.py @@ -1,52 +1,52 @@ -from unittest.mock import Mock -from homeassistant.components.climate.const import HVACMode -import pytest - -from salusfy import (ThermostatEntity, State, WebClient) - -# pylint: disable=missing-function-docstring - - -@pytest.fixture(name="mock_client") -def mock_client_fixture(): - state = State() - state.current_temperature = 15.2 - state.target_temperature = 33.2 - - mock = Mock(WebClient) - mock.get_state.return_value = state - - return mock - - -@pytest.mark.asyncio -async def test_entity_returns_target_temp_from_web_client(mock_client): - target = ThermostatEntity('mock', mock_client) - - await target.async_update() - - assert target.target_temperature == 33.2 - - -@pytest.mark.asyncio -async def test_entity_delegates_set_temperature_web_client(mock_client): - target = ThermostatEntity('mock', mock_client) - - await target.async_update() - - await target.async_set_temperature(temperature=29.9) - - mock_client.set_temperature.assert_called_once_with(29.9) - assert target.target_temperature == 29.9 - - -@pytest.mark.asyncio -async def test_entity_delegates_set_hvac_mode_to_web_client(mock_client): - target = ThermostatEntity('mock', mock_client) - - await target.async_update() - - await target.async_set_hvac_mode(hvac_mode=HVACMode.HEAT) - - mock_client.set_hvac_mode.assert_called_once_with(HVACMode.HEAT) - assert target.hvac_mode == HVACMode.HEAT +from unittest.mock import Mock +from homeassistant.components.climate.const import HVACMode +import pytest + +from salusfy import (ThermostatEntity, State, WebClient) + +# pylint: disable=missing-function-docstring + + +@pytest.fixture(name="mock_client") +def mock_client_fixture(): + state = State() + state.current_temperature = 15.2 + state.target_temperature = 33.2 + + mock = Mock(WebClient) + mock.get_state.return_value = state + + return mock + + +@pytest.mark.asyncio +async def test_entity_returns_target_temp_from_web_client(mock_client): + target = ThermostatEntity('mock', mock_client) + + await target.async_update() + + assert target.target_temperature == 33.2 + + +@pytest.mark.asyncio +async def test_entity_delegates_set_temperature_web_client(mock_client): + target = ThermostatEntity('mock', mock_client) + + await target.async_update() + + await target.async_set_temperature(temperature=29.9) + + mock_client.set_temperature.assert_called_once_with(29.9) + assert target.target_temperature == 29.9 + + +@pytest.mark.asyncio +async def test_entity_delegates_set_hvac_mode_to_web_client(mock_client): + target = ThermostatEntity('mock', mock_client) + + await target.async_update() + + await target.async_set_hvac_mode(hvac_mode=HVACMode.HEAT) + + mock_client.set_hvac_mode.assert_called_once_with(HVACMode.HEAT) + assert target.hvac_mode == HVACMode.HEAT diff --git a/tests/test_web_client.py b/custom_components/tests/test_web_client.py similarity index 100% rename from tests/test_web_client.py rename to custom_components/tests/test_web_client.py diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..e2fbb5a --- /dev/null +++ b/hacs.json @@ -0,0 +1,5 @@ +{ + "name": "Salusfy Thermostat", + "content_in_root": false, + "render_readme": true +} \ No newline at end of file diff --git a/run.py b/run.py index 7c57fbe..057a242 100644 --- a/run.py +++ b/run.py @@ -12,10 +12,10 @@ from homeassistant.components.climate.const import HVACMode -from salusfy import climate -from tests.test_climate import MockHass -from tests.config_adapter import ConfigAdapter -from tests.entity_registry import EntityRegistry +from custom_components.salusfy import climate +from custom_components.tests.test_climate import MockHass +from custom_components.tests.config_adapter import ConfigAdapter +from custom_components.tests.entity_registry import EntityRegistry import config diff --git a/salusfy/manifest.json b/salusfy/manifest.json deleted file mode 100644 index 1eff3f4..0000000 --- a/salusfy/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "salusfy", - "name": "Salus Thermostat", - "version": "0.3.0", - "documentation": "https://github.com/floringhimie/salusfy", - "issue_tracker": "https://github.com/floringhimie/salusfy/issues", - "requirements": [], - "dependencies": [], - "codeowners": [ - "@floringhimie" - ], - "iot_class": "cloud_polling" -} \ No newline at end of file