Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fil pilote #495

Draft
wants to merge 1 commit into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/pyatmo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
SCHEDULES = "schedules"
EVENTS = "events"

COMFORT = "comfort"
AWAY = "away"

STATION_TEMPERATURE_TYPE = "temperature"
STATION_PRESSURE_TYPE = "pressure"
Expand Down
23 changes: 17 additions & 6 deletions src/pyatmo/modules/legrand.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging

from pyatmo.modules.module import (
ApplianceTypeMixin,
BatteryMixin,
ContactorMixin,
Dimmer,
Expand Down Expand Up @@ -35,19 +36,19 @@ class NLT(FirmwareMixin, BatteryMixin, Module):
"""Legrand global remote control."""


class NLP(Switch, HistoryMixin, PowerMixin, OffloadMixin, Module):
class NLP(Switch, HistoryMixin, PowerMixin, OffloadMixin, ApplianceTypeMixin, Module):
"""Legrand plug."""


class NLPM(Switch):
class NLPM(Switch, ApplianceTypeMixin):
"""Legrand mobile plug."""


class NLPO(ContactorMixin, OffloadMixin, Switch):
class NLPO(ContactorMixin, OffloadMixin, ApplianceTypeMixin, Switch):
"""Legrand contactor."""


class NLPT(Switch):
class NLPT(Switch, ApplianceTypeMixin):
"""Legrand latching relay/teleruptor."""


Expand Down Expand Up @@ -107,7 +108,15 @@ class NLPS(FirmwareMixin, PowerMixin, EnergyMixin, Module):
"""Legrand / BTicino smart load shedder."""


class NLC(FirmwareMixin, SwitchMixin, HistoryMixin, PowerMixin, OffloadMixin, Module):
class NLC(
FirmwareMixin,
SwitchMixin,
HistoryMixin,
PowerMixin,
OffloadMixin,
ApplianceTypeMixin,
Module,
):
"""Legrand / BTicino cable outlet."""


Expand Down Expand Up @@ -159,7 +168,9 @@ class NLTS(Module):
"""NLTS motion sensor."""


class NLPD(FirmwareMixin, SwitchMixin, EnergyMixin, PowerMixin, Module):
class NLPD(
FirmwareMixin, SwitchMixin, EnergyMixin, PowerMixin, ApplianceTypeMixin, Module
):
"""NLPD dry contact."""


Expand Down
3 changes: 2 additions & 1 deletion src/pyatmo/modules/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"device_category",
"device_type",
"features",
"appliance_type",
}


Expand Down Expand Up @@ -286,7 +287,7 @@ def __init__(self, home: Home, module: ModuleT):
"""Initialize appliance type mixin."""

super().__init__(home, module) # type: ignore # mypy issue 4335
self.appliance_type: str | None = None
self.appliance_type: str | None = module.get("appliance_type", None)


class EnergyMixin(EntityBase):
Expand Down
18 changes: 16 additions & 2 deletions src/pyatmo/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Room(NetatmoBase):
reachable: bool | None = None
therm_setpoint_temperature: float | None = None
therm_setpoint_mode: str | None = None
therm_setpoint_fp: str | None = None
therm_measured_temperature: float | None = None
therm_setpoint_start_time: int | None = None
therm_setpoint_end_time: int | None = None
Expand Down Expand Up @@ -71,10 +72,14 @@ def update_topology(self, raw_data: RawData) -> None:
def evaluate_device_type(self) -> None:
"""Evaluate the device type of the room."""

has_radiator = False

for module in self.modules.values():
self.device_types.add(module.device_type)
if module.device_category is not None:
self.features.add(module.device_category.name)
if module.device_type == "NLC" and module.appliance_type == "radiator":
has_radiator = True

if "OTM" in self.device_types:
self.climate_type = DeviceType.OTM
Expand All @@ -85,6 +90,8 @@ def evaluate_device_type(self) -> None:
elif "BNS" in self.device_types:
self.climate_type = DeviceType.BNS
self.features.add("humidity")
elif "NLC" in self.device_types and has_radiator:
self.climate_type = DeviceType.NLC

def update(self, raw_data: RawData) -> None:
"""Update room data."""
Expand All @@ -94,18 +101,20 @@ def update(self, raw_data: RawData) -> None:
self.reachable = raw_data.get("reachable")
self.therm_measured_temperature = raw_data.get("therm_measured_temperature")
self.therm_setpoint_mode = raw_data.get("therm_setpoint_mode")
self.therm_setpoint_fp = raw_data.get("therm_setpoint_fp")
self.therm_setpoint_temperature = raw_data.get("therm_setpoint_temperature")
self.therm_setpoint_start_time = raw_data.get("therm_setpoint_start_time")
self.therm_setpoint_end_time = raw_data.get("therm_setpoint_end_time")

async def async_therm_manual(
self,
temp: float | None = None,
fp: str | None = None,
end_time: int | None = None,
) -> None:
"""Set room temperature set point to manual."""

await self.async_therm_set(MANUAL, temp, end_time)
await self.async_therm_set(MANUAL, temp, fp, end_time)

async def async_therm_home(self, end_time: int | None = None) -> None:
"""Set room temperature set point to home."""
Expand All @@ -121,6 +130,7 @@ async def async_therm_set(
self,
mode: str,
temp: float | None = None,
fp: str | None = None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is fp? Can we find a better name here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fil pilote (Pilot wire in english ?)
I've chose this for the consistency with what's used in the API but I'd be eager to change :

For example the /homestatus endpoint returns this :

{
  status:"ok"
  time_server:1712686604
  body: {
    home: {
      id:"xxxx"
      rooms:[{
        id:"xxxx"
        therm_setpoint_end_time:1712693783
        therm_setpoint_fp:"frost_guard"
        therm_setpoint_mode:"home"
      }

Copy link
Collaborator

@cgtobi cgtobi Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, still I can't make much sense of that name. Not your fault, clearly, don't get me wrong. Could you explain the purpose? I can't make sense of the docs on that. Could it be 'Room heating configuration' or 'Home heating mode' (fp) vs. 'Heating schedule' (mode)?

Copy link
Contributor Author

@NatMarchand NatMarchand Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In France we have a standard for electric radiator which is fil pilote. It's a fourth wire which sends commands to a radiator (comfort, eco, frost guard, off).
When you have a Legrand cable outlet configured as a radiator, you can drive the radiator. However, in the API the command is configured on the room as you'd do for a thermostat (without a temperature).
That's what I'm trying to achieve on my PR : make the rooms available as thermostat in home assistant and be able to set commands on radiators.
It can be seen as a "room heating preset" and can be driven in the Legrand app by a schedule.
image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just name it something like pilot wire to have a speaking name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure ! Will do ;)

end_time: int | None = None,
) -> None:
"""Set room temperature set point."""
Expand All @@ -133,12 +143,13 @@ async def async_therm_set(
await self._async_set_thermpoint(mode, temp, end_time)

else:
await self._async_therm_set(mode, temp, end_time)
await self._async_therm_set(mode, temp, fp, end_time)

async def _async_therm_set(
self,
mode: str,
temp: float | None = None,
fp: str | None = None,
end_time: int | None = None,
) -> bool:
"""Set room temperature set point (OTM)."""
Expand All @@ -158,6 +169,9 @@ async def _async_therm_set(
if end_time:
json_therm_set["rooms"][0]["therm_setpoint_end_time"] = end_time

if fp:
json_therm_set["rooms"][0]["therm_setpoint_fp"] = fp

return await self.home.async_set_state(json_therm_set)

async def _async_set_thermpoint(
Expand Down