Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,12 @@ omit =
homeassistant/components/izone/climate.py
homeassistant/components/izone/discovery.py
homeassistant/components/joaoapps_join/*
homeassistant/components/juicenet/__init__.py
homeassistant/components/juicenet/device.py
homeassistant/components/juicenet/entity.py
homeassistant/components/juicenet/number.py
homeassistant/components/juicenet/sensor.py
homeassistant/components/juicenet/switch.py
homeassistant/components/justnimbus/coordinator.py
homeassistant/components/justnimbus/entity.py
homeassistant/components/justnimbus/sensor.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@ build.json @home-assistant/supervisor
/tests/components/jellyfin/ @j-stienstra @ctalkington
/homeassistant/components/jewish_calendar/ @tsvi
/tests/components/jewish_calendar/ @tsvi
/homeassistant/components/juicenet/ @jesserockz
/tests/components/juicenet/ @jesserockz
/homeassistant/components/justnimbus/ @kvanzuijlen
/tests/components/justnimbus/ @kvanzuijlen
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
Expand Down
112 changes: 91 additions & 21 deletions homeassistant/components/juicenet/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,107 @@
"""The JuiceNet integration."""
from __future__ import annotations
from datetime import timedelta
import logging

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
import aiohttp
from pyjuicenet import Api, TokenError
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR
from .device import JuiceNetApi

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]

CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
),
extra=vol.ALLOW_EXTRA,
)


DOMAIN = "juicenet"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the JuiceNet component."""
conf = config.get(DOMAIN)
hass.data.setdefault(DOMAIN, {})

if not conf:
return True

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
)
)
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up JuiceNet from a config entry."""
ir.async_create_issue(

config = entry.data

session = async_get_clientsession(hass)

access_token = config[CONF_ACCESS_TOKEN]
api = Api(access_token, session)

juicenet = JuiceNetApi(api)

try:
await juicenet.setup()
except TokenError as error:
_LOGGER.error("JuiceNet Error %s", error)
return False
except aiohttp.ClientError as error:
_LOGGER.error("Could not reach the JuiceNet API %s", error)
raise ConfigEntryNotReady from error

if not juicenet.devices:
_LOGGER.error("No JuiceNet devices found for this account")
return False
_LOGGER.info("%d JuiceNet device(s) found", len(juicenet.devices))

async def async_update_data():
"""Update all device states from the JuiceNet API."""
for device in juicenet.devices:
await device.update_state(True)
return True

coordinator = DataUpdateCoordinator(
hass,
DOMAIN,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"entries": "/config/integrations/integration/juicenet",
},
_LOGGER,
name="JuiceNet",
update_method=async_update_data,
update_interval=timedelta(seconds=30),
)

await coordinator.async_config_entry_first_refresh()

hass.data[DOMAIN][entry.entry_id] = {
JUICENET_API: juicenet,
JUICENET_COORDINATOR: coordinator,
}

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)

return True
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
69 changes: 68 additions & 1 deletion homeassistant/components/juicenet/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,78 @@
"""Config flow for JuiceNet integration."""
import logging

import aiohttp
from pyjuicenet import Api, TokenError
import voluptuous as vol

from homeassistant import core, exceptions
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.

Data has the keys from DATA_SCHEMA with values provided by the user.
"""
session = async_get_clientsession(hass)
juicenet = Api(data[CONF_ACCESS_TOKEN], session)

try:
await juicenet.get_devices()
except TokenError as error:
_LOGGER.error("Token Error %s", error)
raise InvalidAuth from error
except aiohttp.ClientError as error:
_LOGGER.error("Error connecting %s", error)
raise CannotConnect from error

from . import DOMAIN
# Return info that you want to store in the config entry.
return {"title": "JuiceNet"}


class JuiceNetConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for JuiceNet."""

VERSION = 1

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN])
self._abort_if_unique_id_configured()

try:
info = await validate_input(self.hass, user_input)
return self.async_create_entry(title=info["title"], data=user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)

async def async_step_import(self, user_input):
"""Handle import."""
return await self.async_step_user(user_input)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
6 changes: 6 additions & 0 deletions homeassistant/components/juicenet/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants used by the JuiceNet component."""

DOMAIN = "juicenet"

JUICENET_API = "juicenet_api"
JUICENET_COORDINATOR = "juicenet_coordinator"
19 changes: 19 additions & 0 deletions homeassistant/components/juicenet/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""


class JuiceNetApi:
"""Represent a connection to JuiceNet."""

def __init__(self, api):
"""Create an object from the provided API instance."""
self.api = api
self._devices = []

async def setup(self):
"""JuiceNet device setup.""" # noqa: D403
self._devices = await self.api.get_devices()

@property
def devices(self) -> list:
"""Get a list of devices managed by this account."""
return self._devices
34 changes: 34 additions & 0 deletions homeassistant/components/juicenet/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Adapter to wrap the pyjuicenet api for home assistant."""

from pyjuicenet import Charger

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)

from .const import DOMAIN


class JuiceNetDevice(CoordinatorEntity):
"""Represent a base JuiceNet device."""

_attr_has_entity_name = True

def __init__(
self, device: Charger, key: str, coordinator: DataUpdateCoordinator
) -> None:
"""Initialise the sensor."""
super().__init__(coordinator)
self.device = device
self.key = key
self._attr_unique_id = f"{device.id}-{key}"
self._attr_device_info = DeviceInfo(
configuration_url=(
f"https://home.juice.net/Portal/Details?unitID={device.id}"
),
identifiers={(DOMAIN, device.id)},
manufacturer="JuiceNet",
name=device.name,
)
7 changes: 4 additions & 3 deletions homeassistant/components/juicenet/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"domain": "juicenet",
"name": "JuiceNet",
"codeowners": [],
"codeowners": ["@jesserockz"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/juicenet",
"integration_type": "system",
"iot_class": "cloud_polling",
"requirements": []
"loggers": ["pyjuicenet"],
"requirements": ["python-juicenet==1.1.0"]
}
Loading