mirror of
https://github.com/home-assistant/core.git
synced 2025-04-28 19:27:51 +00:00

* Add Freebox cameras * Apply suggestions from code review add code corrections after PR review Co-authored-by: Quentame <polletquentin74@me.com> * Update base_class.py * add some code syntax corrections add unit tests * add unit tests * add syntax changes * Update homeassistant/components/freebox/router.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/router.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/base_class.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/router.py Co-authored-by: Quentame <polletquentin74@me.com> * clear code and add minor changes * correct syntax error and check home granted access * typing functions * Update tests/components/freebox/conftest.py don't needed, and will fix tests. Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py Rename _volume_micro variable Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py Use const not literal Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py set to true not needed Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py use _attr_supported_features instead _supported_features Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py overload the entity with command_flip property and set_flip not needed Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py Cameras does not default to False, Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py delete this function because is not needed Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py Co-authored-by: Quentame <polletquentin74@me.com> * consts, rollback _command flip is protected var * VALUE_NOT_SET does not exists anymore * Use HOME_COMPATIBLE_PLATFORMS * Rename FreeboxHomeBaseClass to FreeboxHomeEntity * Update Freebox Home comment * Use CATEGORY_TO_MODEL to set model attr of FreeboxHomeEntity * Use Home API from the router * Add SERVICE_FLIP const * Use SERVICE_FLIP const * Fix typo in HOME_COMPATIBLE_PLATFORMS * fix somme code issues * use SERVICE_FLIP (lost in merge) * use _attr_device_info * clear code * HOME_COMPATIBLE_PLATFORMS is a list * Update homeassistant/components/freebox/home_base.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/home_base.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/config_flow.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/home_base.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/home_base.py Co-authored-by: Quentame <polletquentin74@me.com> * clear config_flow permission * Update homeassistant/components/freebox/home_base.py Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py Co-authored-by: Quentame <polletquentin74@me.com> * add untested files to. coveragerc * clear unused attributes * add not tested file camera.py * clear unusued const * add extra_state_attributes * Update .coveragerc Co-authored-by: Quentame <polletquentin74@me.com> * Update homeassistant/components/freebox/camera.py Co-authored-by: Quentame <polletquentin74@me.com> * fetch _flip * del flip service * add device_info via_device * Update .coveragerc * Update .coveragerc * Update .coveragerc * Update .coveragerc * Remove flip reference * Fix issue on router without Home API * Fix "Home access is not granted" log repeats every 30s * Fix sensor device_info --------- Co-authored-by: Quentame <polletquentin74@me.com>
247 lines
8.1 KiB
Python
247 lines
8.1 KiB
Python
"""Represent the Freebox router and its devices and sensors."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from contextlib import suppress
|
|
from datetime import datetime
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from freebox_api import Freepybox
|
|
from freebox_api.api.call import Call
|
|
from freebox_api.api.home import Home
|
|
from freebox_api.api.wifi import Wifi
|
|
from freebox_api.exceptions import HttpRequestError, NotOpenError
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
from homeassistant.helpers.entity import DeviceInfo
|
|
from homeassistant.helpers.storage import Store
|
|
from homeassistant.util import slugify
|
|
|
|
from .const import (
|
|
API_VERSION,
|
|
APP_DESC,
|
|
CONNECTION_SENSORS_KEYS,
|
|
DOMAIN,
|
|
HOME_COMPATIBLE_PLATFORMS,
|
|
STORAGE_KEY,
|
|
STORAGE_VERSION,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def get_api(hass: HomeAssistant, host: str) -> Freepybox:
|
|
"""Get the Freebox API."""
|
|
freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path
|
|
|
|
if not os.path.exists(freebox_path):
|
|
await hass.async_add_executor_job(os.makedirs, freebox_path)
|
|
|
|
token_file = Path(f"{freebox_path}/{slugify(host)}.conf")
|
|
|
|
return Freepybox(APP_DESC, token_file, API_VERSION)
|
|
|
|
|
|
class FreeboxRouter:
|
|
"""Representation of a Freebox router."""
|
|
|
|
def __init__(
|
|
self,
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
api: Freepybox,
|
|
freebox_config: Mapping[str, Any],
|
|
) -> None:
|
|
"""Initialize a Freebox router."""
|
|
self.hass = hass
|
|
self._host = entry.data[CONF_HOST]
|
|
self._port = entry.data[CONF_PORT]
|
|
|
|
self._api: Freepybox = api
|
|
self.name: str = freebox_config["model_info"]["pretty_name"]
|
|
self.mac: str = freebox_config["mac"]
|
|
self._sw_v: str = freebox_config["firmware_version"]
|
|
self._attrs: dict[str, Any] = {}
|
|
|
|
self.devices: dict[str, dict[str, Any]] = {}
|
|
self.disks: dict[int, dict[str, Any]] = {}
|
|
self.sensors_temperature: dict[str, int] = {}
|
|
self.sensors_connection: dict[str, float] = {}
|
|
self.call_list: list[dict[str, Any]] = []
|
|
self.home_granted = True
|
|
self.home_devices: dict[str, Any] = {}
|
|
self.listeners: list[dict[str, Any]] = []
|
|
|
|
async def update_all(self, now: datetime | None = None) -> None:
|
|
"""Update all Freebox platforms."""
|
|
await self.update_device_trackers()
|
|
await self.update_sensors()
|
|
await self.update_home_devices()
|
|
|
|
async def update_device_trackers(self) -> None:
|
|
"""Update Freebox devices."""
|
|
new_device = False
|
|
fbx_devices: list[dict[str, Any]] = await self._api.lan.get_hosts_list()
|
|
|
|
# Adds the Freebox itself
|
|
fbx_devices.append(
|
|
{
|
|
"primary_name": self.name,
|
|
"l2ident": {"id": self.mac},
|
|
"vendor_name": "Freebox SAS",
|
|
"host_type": "router",
|
|
"active": True,
|
|
"attrs": self._attrs,
|
|
}
|
|
)
|
|
|
|
for fbx_device in fbx_devices:
|
|
device_mac = fbx_device["l2ident"]["id"]
|
|
|
|
if self.devices.get(device_mac) is None:
|
|
new_device = True
|
|
|
|
self.devices[device_mac] = fbx_device
|
|
|
|
async_dispatcher_send(self.hass, self.signal_device_update)
|
|
|
|
if new_device:
|
|
async_dispatcher_send(self.hass, self.signal_device_new)
|
|
|
|
async def update_sensors(self) -> None:
|
|
"""Update Freebox sensors."""
|
|
# System sensors
|
|
syst_datas: dict[str, Any] = await self._api.system.get_config()
|
|
|
|
# According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree.
|
|
# Name and id of sensors may vary under Freebox devices.
|
|
for sensor in syst_datas["sensors"]:
|
|
self.sensors_temperature[sensor["name"]] = sensor.get("value")
|
|
|
|
# Connection sensors
|
|
connection_datas: dict[str, Any] = await self._api.connection.get_status()
|
|
for sensor_key in CONNECTION_SENSORS_KEYS:
|
|
self.sensors_connection[sensor_key] = connection_datas[sensor_key]
|
|
|
|
self._attrs = {
|
|
"IPv4": connection_datas.get("ipv4"),
|
|
"IPv6": connection_datas.get("ipv6"),
|
|
"connection_type": connection_datas["media"],
|
|
"uptime": datetime.fromtimestamp(
|
|
round(datetime.now().timestamp()) - syst_datas["uptime_val"]
|
|
),
|
|
"firmware_version": self._sw_v,
|
|
"serial": syst_datas["serial"],
|
|
}
|
|
|
|
self.call_list = await self._api.call.get_calls_log()
|
|
|
|
await self._update_disks_sensors()
|
|
|
|
async_dispatcher_send(self.hass, self.signal_sensor_update)
|
|
|
|
async def _update_disks_sensors(self) -> None:
|
|
"""Update Freebox disks."""
|
|
# None at first request
|
|
fbx_disks: list[dict[str, Any]] = await self._api.storage.get_disks() or []
|
|
|
|
for fbx_disk in fbx_disks:
|
|
self.disks[fbx_disk["id"]] = fbx_disk
|
|
|
|
async def update_home_devices(self) -> None:
|
|
"""Update Home devices (alarm, light, sensor, switch, remote ...)."""
|
|
if not self.home_granted:
|
|
return
|
|
|
|
try:
|
|
home_nodes: list[Any] = await self.home.get_home_nodes() or []
|
|
except HttpRequestError:
|
|
self.home_granted = False
|
|
_LOGGER.warning("Home access is not granted")
|
|
return
|
|
|
|
new_device = False
|
|
for home_node in home_nodes:
|
|
if home_node["category"] in HOME_COMPATIBLE_PLATFORMS:
|
|
if self.home_devices.get(home_node["id"]) is None:
|
|
new_device = True
|
|
self.home_devices[home_node["id"]] = home_node
|
|
|
|
async_dispatcher_send(self.hass, self.signal_home_device_update)
|
|
|
|
if new_device:
|
|
async_dispatcher_send(self.hass, self.signal_home_device_new)
|
|
|
|
async def reboot(self) -> None:
|
|
"""Reboot the Freebox."""
|
|
await self._api.system.reboot()
|
|
|
|
async def close(self) -> None:
|
|
"""Close the connection."""
|
|
with suppress(NotOpenError):
|
|
await self._api.close()
|
|
|
|
@property
|
|
def device_info(self) -> DeviceInfo:
|
|
"""Return the device information."""
|
|
return DeviceInfo(
|
|
configuration_url=f"https://{self._host}:{self._port}/",
|
|
connections={(CONNECTION_NETWORK_MAC, self.mac)},
|
|
identifiers={(DOMAIN, self.mac)},
|
|
manufacturer="Freebox SAS",
|
|
name=self.name,
|
|
sw_version=self._sw_v,
|
|
)
|
|
|
|
@property
|
|
def signal_device_new(self) -> str:
|
|
"""Event specific per Freebox entry to signal new device."""
|
|
return f"{DOMAIN}-{self._host}-device-new"
|
|
|
|
@property
|
|
def signal_home_device_new(self) -> str:
|
|
"""Event specific per Freebox entry to signal new home device."""
|
|
return f"{DOMAIN}-{self._host}-home-device-new"
|
|
|
|
@property
|
|
def signal_device_update(self) -> str:
|
|
"""Event specific per Freebox entry to signal updates in devices."""
|
|
return f"{DOMAIN}-{self._host}-device-update"
|
|
|
|
@property
|
|
def signal_sensor_update(self) -> str:
|
|
"""Event specific per Freebox entry to signal updates in sensors."""
|
|
return f"{DOMAIN}-{self._host}-sensor-update"
|
|
|
|
@property
|
|
def signal_home_device_update(self) -> str:
|
|
"""Event specific per Freebox entry to signal update in home devices."""
|
|
return f"{DOMAIN}-{self._host}-home-device-update"
|
|
|
|
@property
|
|
def sensors(self) -> dict[str, Any]:
|
|
"""Return sensors."""
|
|
return {**self.sensors_temperature, **self.sensors_connection}
|
|
|
|
@property
|
|
def call(self) -> Call:
|
|
"""Return the call."""
|
|
return self._api.call
|
|
|
|
@property
|
|
def wifi(self) -> Wifi:
|
|
"""Return the wifi."""
|
|
return self._api.wifi
|
|
|
|
@property
|
|
def home(self) -> Home:
|
|
"""Return the home."""
|
|
return self._api.home
|