Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ad9b859
Refactor to use ids in data class
cgtobi Nov 22, 2019
ccaeee1
Use station_id
cgtobi Nov 22, 2019
2e7a5c7
Refactor Netatmo to use oauth
cgtobi Nov 28, 2019
22dc311
Remove old code
cgtobi Nov 28, 2019
0139896
Clean up
cgtobi Nov 28, 2019
f5ea022
Clean up
cgtobi Nov 28, 2019
451d727
Clean up
cgtobi Nov 28, 2019
70b5a6e
Refactor binary sensor
cgtobi Dec 9, 2019
23d44ab
Add initial light implementation
cgtobi Dec 9, 2019
17f42c8
Add discovery
cgtobi Dec 9, 2019
583c0b6
Add set schedule service back in
cgtobi Dec 9, 2019
43ba309
Add discovery via homekit
cgtobi Dec 10, 2019
724bc98
More work on the light
cgtobi Dec 10, 2019
ba19600
Fix set schedule service
cgtobi Dec 11, 2019
e6d27d1
Clean up
cgtobi Dec 11, 2019
f2e5b94
Remove unnecessary code
cgtobi Dec 12, 2019
d5e0577
Add support for multiple entities/accounts
cgtobi Dec 12, 2019
0448949
Fix MANUFACTURER typo
cgtobi Dec 12, 2019
7e0f2b7
Remove multiline inline if statement
cgtobi Dec 12, 2019
10d30c7
Only add tags when camera type is welcome
cgtobi Dec 12, 2019
750d17f
Remove on/off as it's currently broken
cgtobi Dec 12, 2019
2038846
Fix camera turn_on/off
cgtobi Dec 12, 2019
756f31f
Fix debug message
cgtobi Dec 12, 2019
cc18203
Refactor some camera code
cgtobi Dec 12, 2019
5a40241
Refactor camera methods
cgtobi Dec 12, 2019
622eedb
Remove old code
cgtobi Dec 12, 2019
1b59e19
Rename method
cgtobi Dec 12, 2019
c2536fa
Update persons regularly
cgtobi Dec 12, 2019
e80e457
Remove unused code
cgtobi Dec 12, 2019
f3c743c
Refactor method
cgtobi Dec 12, 2019
7f72177
Fix isort
cgtobi Dec 12, 2019
4216de9
Add english strings
cgtobi Dec 13, 2019
c560a74
Catch NoDevice exception
cgtobi Dec 13, 2019
159be98
Fix unique id and only add sensors for tags if present
cgtobi Dec 13, 2019
85f767e
Address comments
cgtobi Dec 13, 2019
fe41285
Remove ToDo comment
cgtobi Dec 13, 2019
e37f2e0
Add set_light_auto back in
cgtobi Dec 17, 2019
cb5068b
Add debug info
cgtobi Dec 17, 2019
21eb950
Fix multiple camera issue
cgtobi Dec 17, 2019
90fbf8f
Move camera light service to camera
cgtobi Dec 18, 2019
03b7a54
Only allow camera entities
cgtobi Dec 19, 2019
f18a227
Make test pass
cgtobi Dec 20, 2019
eb50f2b
Upgrade pyatmo module to 3.2.0
cgtobi Jan 2, 2020
038dbc5
Update requirements
cgtobi Jan 2, 2020
7acc035
Remove list comprehension
cgtobi Jan 2, 2020
1fbcafd
Remove guideline violating code
cgtobi Jan 2, 2020
07e52a7
Remove stale code
cgtobi Jan 2, 2020
a8d182a
Rename devices to entities
cgtobi Jan 2, 2020
ec72262
Remove light platform
cgtobi Jan 3, 2020
7c066d1
Remove commented code
cgtobi Jan 3, 2020
0ec0568
Exclude files from coverage
cgtobi Jan 3, 2020
aee09d3
Remove unused code
cgtobi Jan 3, 2020
dbfae85
Fix unique id
cgtobi Jan 3, 2020
fa10785
Address comments
cgtobi Jan 9, 2020
62b6bfd
Fix comments
cgtobi Jan 9, 2020
86124f0
Exclude sensor as well
cgtobi Jan 9, 2020
5c573bf
Add another test
cgtobi Jan 9, 2020
f32424e
Use core interfaces
cgtobi Jan 10, 2020
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
9 changes: 7 additions & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,13 @@ omit =
homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nello/lock.py
homeassistant/components/nest/*
homeassistant/components/netatmo/*
homeassistant/components/netatmo_public/sensor.py
Comment thread
MartinHjelmare marked this conversation as resolved.
homeassistant/components/netatmo/__init__.py
homeassistant/components/netatmo/binary_sensor.py
homeassistant/components/netatmo/api.py
homeassistant/components/netatmo/camera.py
Comment thread
cgtobi marked this conversation as resolved.
homeassistant/components/netatmo/climate.py
homeassistant/components/netatmo/const.py
homeassistant/components/netatmo/sensor.py
homeassistant/components/netdata/sensor.py
homeassistant/components/netgear/device_tracker.py
homeassistant/components/netgear_lte/*
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ homeassistant/components/neato/* @dshokouhi @Santobert
homeassistant/components/nello/* @pschmitt
homeassistant/components/ness_alarm/* @nickw444
homeassistant/components/nest/* @awarecan
homeassistant/components/netatmo/* @cgtobi
homeassistant/components/netdata/* @fabaff
homeassistant/components/nextbus/* @vividboarder
homeassistant/components/nilu/* @hfurubotten
Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/netatmo/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"title": "Netatmo",
"step": {
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"already_setup": "You can only configure one Netatmo account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Netatmo component is not configured. Please follow the documentation."
},
"create_entry": {
"default": "Successfully authenticated with Netatmo."
}
}
}
302 changes: 51 additions & 251 deletions homeassistant/components/netatmo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,286 +1,86 @@
"""Support for the Netatmo devices."""
from datetime import timedelta
"""The Netatmo integration."""
import asyncio
import logging
from urllib.error import HTTPError

import pyatmo
import voluptuous as vol

from homeassistant.const import (
CONF_API_KEY,
CONF_DISCOVERY,
CONF_PASSWORD,
CONF_URL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv

from .const import DATA_NETATMO_AUTH, DOMAIN
from . import api, config_flow
from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN

_LOGGER = logging.getLogger(__name__)

DATA_PERSONS = "netatmo_persons"
DATA_WEBHOOK_URL = "netatmo_webhook_url"

CONF_SECRET_KEY = "secret_key"
CONF_WEBHOOKS = "webhooks"

SERVICE_ADDWEBHOOK = "addwebhook"
SERVICE_DROPWEBHOOK = "dropwebhook"
SERVICE_SETSCHEDULE = "set_schedule"

NETATMO_AUTH = None
NETATMO_WEBHOOK_URL = None

DEFAULT_PERSON = "Unknown"
DEFAULT_DISCOVERY = True
DEFAULT_WEBHOOKS = False

EVENT_PERSON = "person"
EVENT_MOVEMENT = "movement"
EVENT_HUMAN = "human"
EVENT_ANIMAL = "animal"
EVENT_VEHICLE = "vehicle"

EVENT_BUS_PERSON = "netatmo_person"
EVENT_BUS_MOVEMENT = "netatmo_movement"
EVENT_BUS_HUMAN = "netatmo_human"
EVENT_BUS_ANIMAL = "netatmo_animal"
EVENT_BUS_VEHICLE = "netatmo_vehicle"
EVENT_BUS_OTHER = "netatmo_other"

ATTR_ID = "id"
ATTR_PSEUDO = "pseudo"
ATTR_NAME = "name"
ATTR_EVENT_TYPE = "event_type"
ATTR_MESSAGE = "message"
ATTR_CAMERA_ID = "camera_id"
ATTR_HOME_NAME = "home_name"
ATTR_PERSONS = "persons"
ATTR_IS_KNOWN = "is_known"
ATTR_FACE_URL = "face_url"
ATTR_SNAPSHOT_URL = "snapshot_url"
ATTR_VIGNETTE_URL = "vignette_url"
ATTR_SCHEDULE = "schedule"

MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_SECRET_KEY): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_WEBHOOKS, default=DEFAULT_WEBHOOKS): cv.boolean,
vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)

SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({vol.Optional(CONF_URL): cv.string})

SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({})

SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string})


def setup(hass, config):
"""Set up the Netatmo devices."""

hass.data[DATA_PERSONS] = {}
try:
auth = pyatmo.ClientAuth(
config[DOMAIN][CONF_API_KEY],
config[DOMAIN][CONF_SECRET_KEY],
config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD],
"read_station read_camera access_camera "
"read_thermostat write_thermostat "
"read_presence access_presence read_homecoach",
)
except HTTPError:
_LOGGER.error("Unable to connect to Netatmo API")
return False

try:
home_data = pyatmo.HomeData(auth)
except pyatmo.NoDevice:
home_data = None
_LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE)

# Store config to be used during entry setup
hass.data[DATA_NETATMO_AUTH] = auth

if config[DOMAIN][CONF_DISCOVERY]:
for component in "camera", "sensor", "binary_sensor", "climate":
discovery.load_platform(hass, component, DOMAIN, {}, config)
PLATFORMS = ["binary_sensor", "camera", "climate", "sensor"]

if config[DOMAIN][CONF_WEBHOOKS]:
webhook_id = hass.components.webhook.async_generate_id()
hass.data[DATA_WEBHOOK_URL] = hass.components.webhook.async_generate_url(
webhook_id
)
hass.components.webhook.async_register(
DOMAIN, "Netatmo", webhook_id, handle_webhook
)
auth.addwebhook(hass.data[DATA_WEBHOOK_URL])
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, dropwebhook)

def _service_addwebhook(service):
"""Service to (re)add webhooks during runtime."""
url = service.data.get(CONF_URL)
if url is None:
url = hass.data[DATA_WEBHOOK_URL]
_LOGGER.info("Adding webhook for URL: %s", url)
auth.addwebhook(url)

hass.services.register(
DOMAIN,
SERVICE_ADDWEBHOOK,
_service_addwebhook,
schema=SCHEMA_SERVICE_ADDWEBHOOK,
)

def _service_dropwebhook(service):
"""Service to drop webhooks during runtime."""
_LOGGER.info("Dropping webhook")
auth.dropwebhook()
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Netatmo component."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_PERSONS] = {}

hass.services.register(
DOMAIN,
SERVICE_DROPWEBHOOK,
_service_dropwebhook,
schema=SCHEMA_SERVICE_DROPWEBHOOK,
)

def _service_setschedule(service):
"""Service to change current home schedule."""
schedule_name = service.data.get(ATTR_SCHEDULE)
home_data.switchHomeSchedule(schedule=schedule_name)
_LOGGER.info("Set home schedule to %s", schedule_name)
if DOMAIN not in config:
return True

if home_data is not None:
hass.services.register(
config_flow.NetatmoFlowHandler.async_register_implementation(
hass,
config_entry_oauth2_flow.LocalOAuth2Implementation(
hass,
DOMAIN,
SERVICE_SETSCHEDULE,
_service_setschedule,
schema=SCHEMA_SERVICE_SETSCHEDULE,
)
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)

return True


def dropwebhook(hass):
"""Drop the webhook subscription."""
auth = hass.data[DATA_NETATMO_AUTH]
auth.dropwebhook()


async def handle_webhook(hass, webhook_id, request):
"""Handle webhook callback."""
try:
data = await request.json()
except ValueError:
return None
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Netatmo from a config entry."""
implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)

_LOGGER.debug("Got webhook data: %s", data)
published_data = {
ATTR_EVENT_TYPE: data.get(ATTR_EVENT_TYPE),
ATTR_HOME_NAME: data.get(ATTR_HOME_NAME),
ATTR_CAMERA_ID: data.get(ATTR_CAMERA_ID),
ATTR_MESSAGE: data.get(ATTR_MESSAGE),
hass.data[DOMAIN][entry.entry_id] = {
AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation)
}
if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON:
for person in data[ATTR_PERSONS]:
published_data[ATTR_ID] = person.get(ATTR_ID)
published_data[ATTR_NAME] = hass.data[DATA_PERSONS].get(
published_data[ATTR_ID], DEFAULT_PERSON
)
published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN)
published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL)
hass.bus.async_fire(EVENT_BUS_PERSON, published_data)
elif data.get(ATTR_EVENT_TYPE) == EVENT_MOVEMENT:
published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL)
published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL)
hass.bus.async_fire(EVENT_BUS_MOVEMENT, published_data)
elif data.get(ATTR_EVENT_TYPE) == EVENT_HUMAN:
published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL)
published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL)
hass.bus.async_fire(EVENT_BUS_HUMAN, published_data)
elif data.get(ATTR_EVENT_TYPE) == EVENT_ANIMAL:
published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL)
published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL)
hass.bus.async_fire(EVENT_BUS_ANIMAL, published_data)
elif data.get(ATTR_EVENT_TYPE) == EVENT_VEHICLE:
hass.bus.async_fire(EVENT_BUS_VEHICLE, published_data)
published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL)
published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL)
else:
hass.bus.async_fire(EVENT_BUS_OTHER, data)


class CameraData:
"""Get the latest data from Netatmo."""

def __init__(self, hass, auth, home=None):
"""Initialize the data object."""
self._hass = hass
self.auth = auth
self.camera_data = None
self.camera_names = []
self.module_names = []
self.home = home
self.camera_type = None
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

def get_camera_names(self):
"""Return all camera available on the API as a list."""
self.camera_names = []
self.update()
if not self.home:
for home in self.camera_data.cameras:
for camera in self.camera_data.cameras[home].values():
self.camera_names.append(camera["name"])
else:
for camera in self.camera_data.cameras[self.home].values():
self.camera_names.append(camera["name"])
return self.camera_names
return True

def get_module_names(self, camera_name):
"""Return all module available on the API as a list."""
self.module_names = []
self.update()
cam_id = self.camera_data.cameraByName(camera=camera_name, home=self.home)["id"]
for module in self.camera_data.modules.values():
if cam_id == module["cam_id"]:
self.module_names.append(module["name"])
return self.module_names

def get_camera_type(self, camera=None, home=None, cid=None):
"""Return camera type for a camera, cid has preference over camera."""
self.camera_type = self.camera_data.cameraType(
camera=camera, home=home, cid=cid
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
return self.camera_type

def get_persons(self):
"""Gather person data for webhooks."""
for person_id, person_data in self.camera_data.persons.items():
self._hass.data[DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the Netatmo API to update the data."""
self.camera_data = pyatmo.CameraData(self.auth, size=100)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

@Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES)
def update_event(self):
"""Call the Netatmo API to update the events."""
self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type)
return unload_ok
Loading