Skip to content

Commit

Permalink
feat: add master relay (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
acesyde authored Feb 16, 2024
1 parent 2266a3d commit af310bf
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 275 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.13.0
rev: v3.14.1
hooks:
- id: commitizen
- id: commitizen-branch
Expand All @@ -12,7 +12,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.15
rev: v0.2.1
hooks:
- id: ruff
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ _Integration to integrate with [MyLight Systems][mylight_systems]._
| `sensor.battery_state` | Current battery state. | kW | :white_check_mark: |
| `sensor.total_green_energy` | Total power consumned (from the production) by you home. | W/h | :white_check_mark: |
| `sensor.grid_returned_energy` | Total power returned to the grid. | W/h | :white_check_mark: |
| `switch.master_relay` | Master relay switch. | N/A | :white_check_mark: |

## Installation

Expand Down
4 changes: 2 additions & 2 deletions custom_components/mylight_systems/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .api.client import DEFAULT_BASE_URL, MyLightApiClient
from .const import DOMAIN, PLATFORMS
from .const import DOMAIN, PLATFORMS, DATA_COORDINATOR
from .coordinator import MyLightSystemsDataUpdateCoordinator


Expand All @@ -22,7 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
local_coordinator = MyLightSystemsDataUpdateCoordinator(hass=hass, client=client)

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = local_coordinator
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: local_coordinator}

# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
await local_coordinator.async_config_entry_first_refresh()
Expand Down
60 changes: 59 additions & 1 deletion custom_components/mylight_systems/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
DEVICES_URL,
MEASURES_TOTAL_URL,
PROFILE_URL,
STATES_URL,
STATES_URL, SWITCH_URL,
)
from .exceptions import (
CommunicationException,
Expand Down Expand Up @@ -198,3 +198,61 @@ async def async_get_battery_state(
return measure

return measure

async def async_turn_off(self, auth_token: str, relay_id: str) -> str:
"""Turn off the switch."""
response = await self._execute_request(
"get",
SWITCH_URL,
params={
"authToken": auth_token,
"id": relay_id,
"on": "false",
},
)

if response["status"] == "error":
if response["error"] == "switch.not.allowed":
return "off"
if response["error"] == "not.authorized":
raise UnauthorizedException()

return response["state"]

async def async_turn_on(self, auth_token: str, relay_id: str) -> str:
"""Turn on the switch."""
response = await self._execute_request(
"get",
SWITCH_URL,
params={
"authToken": auth_token,
"id": relay_id,
"on": "true",
},
)

if response["status"] == "error":
if response["error"] == "switch.not.allowed":
return "on"
if response["error"] == "not.authorized":
raise UnauthorizedException()

return response["state"]

async def async_get_relay_state(
self, auth_token: str, relay_id: str
) -> str | None:
"""Get relay state."""
response = await self._execute_request(
"get", STATES_URL, params={"authToken": auth_token}
)

if response["status"] == "error":
if response["error"] == "not.authorized":
raise UnauthorizedException()

for device in response["deviceStates"]:
if device["deviceId"] == relay_id:
return device["state"]

return None
1 change: 1 addition & 0 deletions custom_components/mylight_systems/api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
DEVICES_URL: str = "/api/devices"
MEASURES_TOTAL_URL: str = "/api/measures/total"
STATES_URL: str = "/api/states"
SWITCH_URL: str = "/api/device/switch"
2 changes: 1 addition & 1 deletion custom_components/mylight_systems/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class InstallationDevices:
__master_report_period: int = 60
__virtual_device_id: str = None
__virtual_battery_id: str = None
__master_relay_id: str = None
__master_relay_id: str | None = None

@property
def master_id(self):
Expand Down
7 changes: 5 additions & 2 deletions custom_components/mylight_systems/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
# General
NAME = "MyLight Systems"
DOMAIN = "mylight_systems"
PLATFORMS = [Platform.SENSOR]
VERSION = "0.0.7"
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
VERSION = "0.1.0"
COORDINATOR = "coordinator"
ATTRIBUTION = "Data provided by https://www.mylight-systems.com/"
SCAN_INTERVAL_IN_MINUTES = 15
Expand All @@ -22,3 +22,6 @@
CONF_SUBSCRIPTION_ID = "subscription_id"
CONF_GRID_TYPE = "grid_type"
CONF_MASTER_RELAY_ID = "master_relay_id"

# Data
DATA_COORDINATOR = "coordinator"
35 changes: 33 additions & 2 deletions custom_components/mylight_systems/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
CONF_VIRTUAL_DEVICE_ID,
DOMAIN,
LOGGER,
SCAN_INTERVAL_IN_MINUTES,
SCAN_INTERVAL_IN_MINUTES, CONF_MASTER_RELAY_ID,
)


Expand All @@ -43,6 +43,7 @@ class MyLightSystemsCoordinatorData(NamedTuple):
msb_discharge: Measure
green_energy: Measure
battery_state: Measure
master_relay_state: str


# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
Expand Down Expand Up @@ -77,6 +78,9 @@ async def _async_update_data(self) -> MyLightSystemsCoordinatorData:
virtual_battery_id = self.config_entry.data[
CONF_VIRTUAL_BATTERY_ID
]
master_relay_id = self.config_entry.data[
CONF_MASTER_RELAY_ID
]

await self.authenticate_user(email, password)

Expand All @@ -88,7 +92,11 @@ async def _async_update_data(self) -> MyLightSystemsCoordinatorData:
self.__auth_token, virtual_battery_id
)

return MyLightSystemsCoordinatorData(
master_relay_state = await self.client.async_get_relay_state(
self.__auth_token, master_relay_id
)

data = MyLightSystemsCoordinatorData(
produced_energy=self.find_measure_by_type(
result, "produced_energy"
),
Expand All @@ -106,7 +114,12 @@ async def _async_update_data(self) -> MyLightSystemsCoordinatorData:
),
green_energy=self.find_measure_by_type(result, "green_energy"),
battery_state=battery_state,
master_relay_state=master_relay_state,
)

self._data = data

return data
except (
UnauthorizedException,
InvalidCredentialsException,
Expand All @@ -126,6 +139,24 @@ async def authenticate_user(self, email, password):
self.__auth_token = result.auth_token
self.__token_expiration = datetime.utcnow() + timedelta(hours=2)

async def turn_on_master_relay(self):
"""Turn on master relay."""
await self.client.async_turn_on(
self.__auth_token, self.config_entry.data[CONF_MASTER_RELAY_ID]
)

async def turn_off_master_relay(self):
"""Turn off master relay."""
await self.client.async_turn_off(
self.__auth_token, self.config_entry.data[CONF_MASTER_RELAY_ID]
)

def master_relay_is_on(self) -> bool:
"""Return true if master relay is on."""
if self._data is not None and self._data.master_relay_state is not None:
return self._data.master_relay_state == "on"
return False

@staticmethod
def find_measure_by_type(
measures: list[Measure], name: str
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mylight_systems/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"documentation": "https://github.com/acesyde/hassio_mylight_integration",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/acesyde/hassio_mylight_integration/issues",
"version": "0.0.7"
"version": "0.1.0"
}
15 changes: 9 additions & 6 deletions custom_components/mylight_systems/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, POWER_KILO_WATT, UnitOfEnergy
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import (
MyLightSystemsCoordinatorData,
MyLightSystemsDataUpdateCoordinator,
Expand Down Expand Up @@ -130,7 +133,7 @@ class MyLightSensorEntityDescription(
key="battery_state",
name="Battery state",
icon="mdi:battery",
native_unit_of_measurement=POWER_KILO_WATT,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: round(data.battery_state.value / 36e2 / 1e3, 2)
if data.battery_state is not None
Expand Down Expand Up @@ -179,9 +182,9 @@ def _calculate_grid_returned_energy(data: MyLightSystemsCoordinatorData) -> floa
return 0


async def async_setup_entry(hass, entry, async_add_devices):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_devices: AddEntitiesCallback) -> None:
"""Configure sensor platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
async_add_devices(
MyLightSystemsSensor(
entry_id=entry.entry_id,
Expand All @@ -199,7 +202,7 @@ def __init__(
self,
entry_id: str,
coordinator: MyLightSystemsDataUpdateCoordinator,
entity_description: SensorEntityDescription,
entity_description: MyLightSensorEntityDescription,
) -> None:
"""Init."""
super().__init__(coordinator)
Expand Down
86 changes: 86 additions & 0 deletions custom_components/mylight_systems/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from dataclasses import dataclass
from typing import Callable, Coroutine, Any

from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import DOMAIN
from .api.exceptions import MyLightSystemsException
from .const import LOGGER, DATA_COORDINATOR, CONF_MASTER_RELAY_ID
from .coordinator import MyLightSystemsDataUpdateCoordinator
from .entity import IntegrationMyLightSystemsEntity


@dataclass(kw_only=True)
class MyLightSystemsSwitchEntityDescription(SwitchEntityDescription):
"""Describes MyLight Systems switch entity."""

is_on_fn: Callable[[MyLightSystemsDataUpdateCoordinator], bool]
turn_on_fn: Callable[[MyLightSystemsDataUpdateCoordinator], Callable[[], Coroutine[Any, Any, None]]]
turn_off_fn: Callable[[MyLightSystemsDataUpdateCoordinator], Callable[[], Coroutine[Any, Any, None]]]


master_relay_switch = MyLightSystemsSwitchEntityDescription(
key="master_relay",
name="Master relay",
icon="mdi:light-switch",
is_on_fn=lambda coordinator: coordinator.master_relay_is_on,
turn_on_fn=lambda coordinator: coordinator.turn_on_master_relay,
turn_off_fn=lambda coordinator: coordinator.turn_off_master_relay,
)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Configure switch platform."""
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]

switches: list[MyLightSystemsSwitchEntityDescription] = []

if entry.data[CONF_MASTER_RELAY_ID] is not None:
switches.append(master_relay_switch)

async_add_entities(
[MyLightSystemsSwitch(entry.entry_id, coordinator, description) for description in switches],
True,
)


class MyLightSystemsSwitch(IntegrationMyLightSystemsEntity, SwitchEntity):
"""Defines a MyLight Systems switch."""

def __init__(
self,
entry_id: ConfigEntry,
coordinator: MyLightSystemsDataUpdateCoordinator,
entity_description: MyLightSystemsSwitchEntityDescription,
) -> None:
"""Initialize MyLight Systems switch."""
super().__init__(coordinator)
self.entity_id = f"{DOMAIN}.{entity_description.key}"
self._attr_unique_id = f"{entry_id}_{entity_description.key}"
self.entity_description = entity_description

@property
def is_on(self):
"""Return true if it is on."""
return self.entity_description.is_on_fn(self.coordinator)()

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
try:
await self.entity_description.turn_off_fn(self.coordinator)()
await self.coordinator.async_request_refresh()
except MyLightSystemsException:
LOGGER.error("An error occurred while turning off MyLight Systems switch")
self._attr_available = False

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
try:
await self.entity_description.turn_on_fn(self.coordinator)()
await self.coordinator.async_request_refresh()
except MyLightSystemsException:
LOGGER.error("An error occurred while turning on MyLight Systems switch")
self._attr_available = False
Loading

0 comments on commit af310bf

Please sign in to comment.