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