mirror of
https://github.com/home-assistant/core.git
synced 2025-08-03 10:38:22 +00:00
add redgtech integration
This commit is contained in:
parent
e0ea5bfc51
commit
8fdd8c0103
@ -402,6 +402,7 @@ homeassistant.components.raspberry_pi.*
|
|||||||
homeassistant.components.rdw.*
|
homeassistant.components.rdw.*
|
||||||
homeassistant.components.recollect_waste.*
|
homeassistant.components.recollect_waste.*
|
||||||
homeassistant.components.recorder.*
|
homeassistant.components.recorder.*
|
||||||
|
homeassistant.components.redgtech.*
|
||||||
homeassistant.components.remote.*
|
homeassistant.components.remote.*
|
||||||
homeassistant.components.renault.*
|
homeassistant.components.renault.*
|
||||||
homeassistant.components.reolink.*
|
homeassistant.components.reolink.*
|
||||||
|
63
homeassistant/components/redgtech/__init__.py
Normal file
63
homeassistant/components/redgtech/__init__.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import logging
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from .const import DOMAIN, API_URL
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.LIGHT]
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Redgtech from a config entry."""
|
||||||
|
_LOGGER.debug("Setting up Redgtech entry: %s", entry.entry_id)
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
"config": entry.data,
|
||||||
|
"entities": []
|
||||||
|
}
|
||||||
|
|
||||||
|
access_token = entry.data.get("access_token")
|
||||||
|
if not access_token:
|
||||||
|
_LOGGER.error("No access token found in config entry")
|
||||||
|
return False
|
||||||
|
|
||||||
|
session = async_get_clientsession(hass)
|
||||||
|
try:
|
||||||
|
async with session.get(f'{API_URL}/home_assistant?access_token={access_token}', timeout=10) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
data = await response.json()
|
||||||
|
_LOGGER.debug("Received data from API: %s", data)
|
||||||
|
|
||||||
|
entities = [
|
||||||
|
{
|
||||||
|
"id": item.get('endpointId', ''),
|
||||||
|
"name": item.get("name", f"Entity {item.get('endpointId', '')}"),
|
||||||
|
"state": "on" if item.get("value", False) else "off",
|
||||||
|
"brightness": item.get("bright", 0),
|
||||||
|
"type": 'light' if 'dim' in item.get('endpointId', '').lower() else 'switch'
|
||||||
|
}
|
||||||
|
for item in data.get("boards", [])
|
||||||
|
]
|
||||||
|
hass.data[DOMAIN][entry.entry_id]["entities"] = entities
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
_LOGGER.debug("Successfully set up Redgtech entry: %s", entry.entry_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except aiohttp.ClientResponseError as e:
|
||||||
|
_LOGGER.error("HTTP error while setting up Redgtech entry: %s - Status: %s", e.message, e.status)
|
||||||
|
return False
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
_LOGGER.error("Client error while setting up Redgtech entry: %s", e)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.exception("Unexpected error setting up Redgtech entry: %s", entry.entry_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
_LOGGER.debug("Unloading Redgtech entry: %s", entry.entry_id)
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
55
homeassistant/components/redgtech/config_flow.py
Normal file
55
homeassistant/components/redgtech/config_flow.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from homeassistant import config_entries
|
||||||
|
import voluptuous as vol
|
||||||
|
import aiohttp
|
||||||
|
import logging
|
||||||
|
from .const import DOMAIN, API_URL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class RedgtechConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Config Flow for Redgtech integration."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial user step for login."""
|
||||||
|
errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
email = user_input.get("email")
|
||||||
|
password = user_input.get("password")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
f'{API_URL}/home_assistant/login',
|
||||||
|
json={'email': email, 'password': password}
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
access_token = data.get("data", {}).get("access_token")
|
||||||
|
if access_token:
|
||||||
|
_LOGGER.info("Login successful")
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title="Redgtech",
|
||||||
|
data={"access_token": access_token}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Login failed: No access token received")
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Login failed: Invalid credentials")
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
_LOGGER.error("Login failed: Cannot connect to server: %s", e)
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required("email"): str,
|
||||||
|
vol.Required("password"): str,
|
||||||
|
}),
|
||||||
|
errors=errors
|
||||||
|
)
|
2
homeassistant/components/redgtech/const.py
Normal file
2
homeassistant/components/redgtech/const.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DOMAIN = "redgtech"
|
||||||
|
API_URL = "https://redgtech-dev.com"
|
127
homeassistant/components/redgtech/light.py
Normal file
127
homeassistant/components/redgtech/light.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from homeassistant.components.light import LightEntity, ColorMode
|
||||||
|
from homeassistant.const import STATE_ON, STATE_OFF, CONF_BRIGHTNESS
|
||||||
|
from .const import API_URL
|
||||||
|
import aiohttp
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up the light platform."""
|
||||||
|
access_token = config_entry.data.get("access_token")
|
||||||
|
if access_token:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f'{API_URL}/home_assistant?access_token={access_token}') as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for item in data.get("boards", []):
|
||||||
|
endpoint_id = item.get('endpointId', '')
|
||||||
|
if 'dim' in endpoint_id:
|
||||||
|
entities.append(RedgtechLight(item, access_token))
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Error fetching data from API: %s", response.status)
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
_LOGGER.error("Error connecting to API: %s", e)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("No access token available")
|
||||||
|
|
||||||
|
|
||||||
|
class RedgtechLight(LightEntity):
|
||||||
|
"""Representation of a dimmable light."""
|
||||||
|
|
||||||
|
def __init__(self, data, token):
|
||||||
|
self._state = STATE_ON if data.get("value", False) else STATE_OFF
|
||||||
|
self._brightness = self._convert_brightness(data.get("bright", 0))
|
||||||
|
self._previous_brightness = self._brightness
|
||||||
|
self._name = data.get("friendlyName")
|
||||||
|
self._endpoint_id = data.get("endpointId")
|
||||||
|
self._description = data.get("description")
|
||||||
|
self._manufacturer = data.get("manufacturerName")
|
||||||
|
self._token = token
|
||||||
|
self._supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||||
|
self._color_mode = ColorMode.BRIGHTNESS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the light."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if the light is on."""
|
||||||
|
return self._state == STATE_ON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of the light."""
|
||||||
|
return self._brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_color_modes(self):
|
||||||
|
"""Return supported color modes."""
|
||||||
|
return self._supported_color_modes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_mode(self):
|
||||||
|
"""Return the color mode of the light."""
|
||||||
|
return self._color_mode
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the light on with optional brightness."""
|
||||||
|
brightness = kwargs.get(CONF_BRIGHTNESS, self._previous_brightness)
|
||||||
|
await self._set_state(STATE_ON, brightness)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the light off."""
|
||||||
|
self._previous_brightness = self._brightness
|
||||||
|
await self._set_state(STATE_OFF)
|
||||||
|
|
||||||
|
async def _set_state(self, state, brightness=None):
|
||||||
|
"""Send the state and brightness to the API to update the light."""
|
||||||
|
id_part, after_id = self._endpoint_id.split("-", 1)
|
||||||
|
number_channel = after_id[-1]
|
||||||
|
type_channel = ''.join(char for char in after_id if char.isalpha())
|
||||||
|
brightness_value = round((brightness / 255) * 100) if brightness else 0
|
||||||
|
state_char = 'l' if state else 'd'
|
||||||
|
if type_channel == "AC":
|
||||||
|
value = f"{number_channel}{state_char}"
|
||||||
|
else:
|
||||||
|
value = f"{type_channel}{number_channel}*{brightness_value}*"
|
||||||
|
|
||||||
|
url = f"{API_URL}/home_assistant/execute/{id_part}?cod=?{value}"
|
||||||
|
headers = {"Authorization": f"{self._token}"}
|
||||||
|
payload = {"state": state}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, headers=headers, json=payload) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
self._state = state
|
||||||
|
if state == STATE_ON:
|
||||||
|
self._brightness = brightness or 255
|
||||||
|
else:
|
||||||
|
self._brightness = 0
|
||||||
|
self.async_write_ha_state()
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Failed to set state for %s, status code: %s", self._name, response.status)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {
|
||||||
|
"endpoint_id": self._endpoint_id,
|
||||||
|
"description": self._description,
|
||||||
|
"manufacturer": self._manufacturer,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _convert_brightness(self, bright_value):
|
||||||
|
"""Convert brightness value from 0-100 to 0-255."""
|
||||||
|
try:
|
||||||
|
return int((int(bright_value) / 100) * 255)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
_LOGGER.error("Invalid brightness value: %s", bright_value)
|
||||||
|
return 0
|
13
homeassistant/components/redgtech/manifest.json
Normal file
13
homeassistant/components/redgtech/manifest.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"domain": "redgtech",
|
||||||
|
"name": "Redgtech",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"codeowners": [],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/redgtech",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"logo": "/brands/redgtech/logo.png",
|
||||||
|
"integration_type": "service",
|
||||||
|
"config_flow": true,
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": []
|
||||||
|
}
|
90
homeassistant/components/redgtech/quality_scale.yaml
Normal file
90
homeassistant/components/redgtech/quality_scale.yaml
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: only entity actions
|
||||||
|
appropriate-polling:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration does not poll
|
||||||
|
brands: done
|
||||||
|
common-modules:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration currently implements only one platform and has no coordinator
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions: done
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration does not subscribe to events
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure:
|
||||||
|
status: done
|
||||||
|
comment: tested by publishing a success message to the topic
|
||||||
|
test-before-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: testing would require to trigger a notification
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions: done
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration has no options
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration only implements a stateless notify entity.
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration only integrates state-less entities
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration currently does not implement authenticated requests
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: todo
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class:
|
||||||
|
status: exempt
|
||||||
|
comment: no suitable device class for the notify entity
|
||||||
|
entity-disabled-by-default:
|
||||||
|
status: exempt
|
||||||
|
comment: only one entity
|
||||||
|
entity-translations:
|
||||||
|
status: exempt
|
||||||
|
comment: the notify entity uses the topic as name, no translation required
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: the integration has no repeairs
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: only one device per entry, is deleted with the entry.
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
12
homeassistant/components/redgtech/services.yaml
Normal file
12
homeassistant/components/redgtech/services.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
login:
|
||||||
|
description: "Log in to the Redgtech service"
|
||||||
|
fields:
|
||||||
|
email:
|
||||||
|
description: "The email address for the Redgtech account"
|
||||||
|
example: "user@example.com"
|
||||||
|
password:
|
||||||
|
description: "The password for the Redgtech account"
|
||||||
|
example: "your_password"
|
||||||
|
|
||||||
|
logout:
|
||||||
|
description: "Log out from the Redgtech service"
|
67
homeassistant/components/redgtech/strings.json
Normal file
67
homeassistant/components/redgtech/strings.json
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "User Configuration",
|
||||||
|
"description": "Please enter your email address.",
|
||||||
|
"data": {
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"generic": {
|
||||||
|
"model": "Model",
|
||||||
|
"ui_managed": "Managed via UI"
|
||||||
|
},
|
||||||
|
"device_automation": {
|
||||||
|
"condition_type": {
|
||||||
|
"is_on": "{entity_name} is on",
|
||||||
|
"is_off": "{entity_name} is off"
|
||||||
|
},
|
||||||
|
"extra_fields": {
|
||||||
|
"above": "Above",
|
||||||
|
"below": "Below",
|
||||||
|
"for": "Duration",
|
||||||
|
"to": "To",
|
||||||
|
"value": "Value",
|
||||||
|
"zone": "Zone"
|
||||||
|
},
|
||||||
|
"trigger_type": {
|
||||||
|
"changed_states": "{entity_name} turned on or off",
|
||||||
|
"turned_on": "{entity_name} turned on",
|
||||||
|
"turned_off": "{entity_name} turned off"
|
||||||
|
},
|
||||||
|
"action_type": {
|
||||||
|
"toggle": "Toggle {entity_name}",
|
||||||
|
"turn_on": "Turn on {entity_name}",
|
||||||
|
"turn_off": "Turn off {entity_name}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"connect": "Connect",
|
||||||
|
"disconnect": "Disconnect",
|
||||||
|
"enable": "Enable",
|
||||||
|
"disable": "Disable",
|
||||||
|
"open": "Open",
|
||||||
|
"close": "Close",
|
||||||
|
"reload": "Reload",
|
||||||
|
"restart": "Restart",
|
||||||
|
"start": "Start",
|
||||||
|
"stop": "Stop",
|
||||||
|
"pause": "Pause",
|
||||||
|
"turn_on": "Turn on",
|
||||||
|
"turn_off": "Turn off",
|
||||||
|
"toggle": "Toggle"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"sunday": "Sunday"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"not_home": "Away"
|
||||||
|
},
|
||||||
|
"config_flow": {}
|
||||||
|
}
|
||||||
|
}
|
103
homeassistant/components/redgtech/switch.py
Normal file
103
homeassistant/components/redgtech/switch.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import logging
|
||||||
|
import aiohttp
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from .const import DOMAIN, API_URL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up the light platform."""
|
||||||
|
access_token = config_entry.data.get("access_token")
|
||||||
|
if access_token:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(f'{API_URL}/home_assistant?access_token={access_token}') as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
entities = []
|
||||||
|
for item in data.get("boards", []):
|
||||||
|
categories = item.get("displayCategories", "")
|
||||||
|
if "SWITCH" in categories:
|
||||||
|
|
||||||
|
entities.append(RedgtechSwitch(item, access_token))
|
||||||
|
async_add_entities(entities)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Error fetching data from API: %s", response.status)
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
_LOGGER.error("Error connecting to API: %s", e)
|
||||||
|
else:
|
||||||
|
_LOGGER.error("No access token available")
|
||||||
|
|
||||||
|
class RedgtechSwitch(SwitchEntity):
|
||||||
|
"""Representation of a Redgtech switch."""
|
||||||
|
|
||||||
|
def __init__(self, data, token):
|
||||||
|
self._state = data.get("value", False)
|
||||||
|
self._name = data.get("friendlyName")
|
||||||
|
self._endpoint_id = data.get("endpointId")
|
||||||
|
self._description = data.get("description")
|
||||||
|
self._manufacturer = data.get("manufacturerName")
|
||||||
|
self._token = token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the switch."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if the switch is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the switch on."""
|
||||||
|
await self._set_state(True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the switch off."""
|
||||||
|
await self._set_state(False)
|
||||||
|
|
||||||
|
async def _set_state(self, state):
|
||||||
|
"""Send the state to the API to update the switch."""
|
||||||
|
id_part, after_id = self._endpoint_id.split("-", 1)
|
||||||
|
value = ''.join(filter(str.isdigit, after_id))
|
||||||
|
state_char = 'l' if state else 'd'
|
||||||
|
url = f"{API_URL}/home_assistant/execute/{id_part}?cod=?{value}{state_char}"
|
||||||
|
headers = {"Authorization": f"{self._token}"}
|
||||||
|
payload = {"state": state}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, headers=headers, json=payload) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
self._state = state
|
||||||
|
self.async_write_ha_state()
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Failed to set state for %s, status code: %s", self._name, response.status)
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Get the latest state of the switch."""
|
||||||
|
id_part, after_id = self._endpoint_id.split("-", 1)
|
||||||
|
value = after_id
|
||||||
|
url = f"{API_URL}/home_assistant?access_token={self._token}"
|
||||||
|
headers = {"Authorization": f"{self._token}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, headers=headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
for board in data.get("boards", []):
|
||||||
|
if board.get("endpointId") == self._endpoint_id:
|
||||||
|
value = board.get("value", False)
|
||||||
|
self._state = bool(value)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Failed to update state for %s, status code: %s",
|
||||||
|
self._name,
|
||||||
|
response.status,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("Error updating state for %s: %s", self._name, str(e))
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -506,6 +506,7 @@ FLOWS = {
|
|||||||
"rapt_ble",
|
"rapt_ble",
|
||||||
"rdw",
|
"rdw",
|
||||||
"recollect_waste",
|
"recollect_waste",
|
||||||
|
"redgtech"
|
||||||
"refoss",
|
"refoss",
|
||||||
"renault",
|
"renault",
|
||||||
"renson",
|
"renson",
|
||||||
|
@ -5174,6 +5174,12 @@
|
|||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"redgtech": {
|
||||||
|
"name": "Redgtech Automação",
|
||||||
|
"integration_type": "service",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"refoss": {
|
"refoss": {
|
||||||
"name": "Refoss",
|
"name": "Refoss",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@ -3856,6 +3856,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.redgtech.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.ridwell.*]
|
[mypy-homeassistant.components.ridwell.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
1
tests/components/redgtech/__init__.py
Normal file
1
tests/components/redgtech/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the Redgtech component."""
|
45
tests/components/redgtech/test_config_flow.py
Normal file
45
tests/components/redgtech/test_config_flow.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from homeassistant import config_entries, data_entry_flow
|
||||||
|
from homeassistant.components.redgtech.config_flow import RedgtechConfigFlow
|
||||||
|
from homeassistant.components.redgtech.const import DOMAIN
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_flow():
|
||||||
|
"""Return a mock config flow."""
|
||||||
|
return RedgtechConfigFlow()
|
||||||
|
|
||||||
|
async def test_show_form(mock_flow):
|
||||||
|
"""Test that the form is shown."""
|
||||||
|
result = await mock_flow.async_step_user()
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
async def test_invalid_auth(mock_flow):
|
||||||
|
"""Test handling of invalid authentication."""
|
||||||
|
with patch("aiohttp.ClientSession.post") as mock_post:
|
||||||
|
mock_post.return_value.__aenter__.return_value.status = 401
|
||||||
|
result = await mock_flow.async_step_user({"email": "test@test.com", "password": "wrongpassword"})
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
async def test_cannot_connect(mock_flow):
|
||||||
|
"""Test handling of connection errors."""
|
||||||
|
with patch("aiohttp.ClientSession.post", side_effect=aiohttp.ClientError):
|
||||||
|
result = await mock_flow.async_step_user({"email": "test@test.com", "password": "password"})
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
async def test_create_entry(mock_flow):
|
||||||
|
"""Test that a config entry is created."""
|
||||||
|
with patch("aiohttp.ClientSession.post") as mock_post:
|
||||||
|
mock_post.return_value.__aenter__.return_value.status = 200
|
||||||
|
mock_post.return_value.__aenter__.return_value.json.return_value = {
|
||||||
|
"data": {"access_token": "test_token"}
|
||||||
|
}
|
||||||
|
result = await mock_flow.async_step_user({"email": "test@test.com", "password": "password"})
|
||||||
|
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "Redgtech"
|
||||||
|
assert result["data"] == {"access_token": "test_token"}
|
82
tests/components/redgtech/test_light.py
Normal file
82
tests/components/redgtech/test_light.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
from homeassistant.components.redgtech.light import RedgtechLight
|
||||||
|
from homeassistant.const import CONF_BRIGHTNESS, STATE_ON, STATE_OFF
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def light_data():
|
||||||
|
return {
|
||||||
|
"endpointId": "dim-1",
|
||||||
|
"value": True,
|
||||||
|
"bright": 50,
|
||||||
|
"friendlyName": "Test Light",
|
||||||
|
"description": "Test Description",
|
||||||
|
"manufacturerName": "Test Manufacturer"
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def access_token():
|
||||||
|
return "test_token"
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def light(light_data, access_token):
|
||||||
|
return RedgtechLight(light_data, access_token)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_light_initial_state(light):
|
||||||
|
assert light.name == "Test Light"
|
||||||
|
assert light.is_on is True
|
||||||
|
assert light.brightness == 127
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_turn_on_light(light):
|
||||||
|
with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get:
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
async def mock_turn_on(**kwargs):
|
||||||
|
light._state = STATE_ON
|
||||||
|
light._brightness = 255
|
||||||
|
|
||||||
|
with patch.object(RedgtechLight, 'async_turn_on', new=AsyncMock(side_effect=mock_turn_on)) as mock_turn_on_method:
|
||||||
|
await light.async_turn_on(brightness=255)
|
||||||
|
mock_turn_on_method.assert_called_once_with(brightness=255)
|
||||||
|
await light.async_turn_on()
|
||||||
|
|
||||||
|
assert light.is_on is True
|
||||||
|
assert light.brightness == 255
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_turn_off_light(light):
|
||||||
|
with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get:
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
async def mock_turn_off():
|
||||||
|
light._state = STATE_OFF
|
||||||
|
light._brightness = 0
|
||||||
|
|
||||||
|
with patch.object(RedgtechLight, 'async_turn_off', new=AsyncMock(side_effect=mock_turn_off)):
|
||||||
|
await light.async_turn_off()
|
||||||
|
|
||||||
|
assert light.is_on is False
|
||||||
|
assert light.brightness == 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_set_brightness_light(light):
|
||||||
|
with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get:
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
async def mock_set_brightness(brightness):
|
||||||
|
light._brightness = brightness
|
||||||
|
light._state = STATE_ON if brightness > 0 else STATE_OFF
|
||||||
|
|
||||||
|
with patch.object(RedgtechLight, 'async_turn_on', new=AsyncMock(side_effect=mock_set_brightness)):
|
||||||
|
await light.async_turn_on(brightness=200)
|
||||||
|
|
||||||
|
assert light.brightness == 200
|
||||||
|
assert light.is_on is True
|
72
tests/components/redgtech/test_switch.py
Normal file
72
tests/components/redgtech/test_switch.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
from homeassistant.components.redgtech.switch import RedgtechSwitch
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def switch_data():
|
||||||
|
return {
|
||||||
|
"value": False,
|
||||||
|
"friendlyName": "Test Switch",
|
||||||
|
"endpointId": "1234-5678",
|
||||||
|
"description": "Test Description",
|
||||||
|
"manufacturerName": "Test Manufacturer"
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def access_token():
|
||||||
|
return "test_access_token"
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def switch(switch_data, access_token):
|
||||||
|
return RedgtechSwitch(switch_data, access_token)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_switch_initial_state(switch):
|
||||||
|
assert switch.name == "Test Switch"
|
||||||
|
assert switch.is_on is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_turn_on_switch(switch):
|
||||||
|
with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get:
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
async def mock_turn_on():
|
||||||
|
switch._state = True
|
||||||
|
|
||||||
|
with patch.object(RedgtechSwitch, 'turn_on', new=AsyncMock(side_effect=mock_turn_on)):
|
||||||
|
await switch.turn_on()
|
||||||
|
|
||||||
|
assert switch.is_on is True
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_turn_off_switch(switch):
|
||||||
|
switch._state = True
|
||||||
|
with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get:
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
async def mock_turn_off():
|
||||||
|
switch._state = False
|
||||||
|
|
||||||
|
with patch.object(RedgtechSwitch, 'turn_off', new=AsyncMock(side_effect=mock_turn_off)):
|
||||||
|
await switch.turn_off()
|
||||||
|
|
||||||
|
assert switch.is_on is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_set_state_switch(switch):
|
||||||
|
with patch("aiohttp.ClientSession.get", new_callable=AsyncMock) as mock_get:
|
||||||
|
mock_response = AsyncMock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
async def mock_set_state(state):
|
||||||
|
switch._state = state
|
||||||
|
|
||||||
|
with patch.object(RedgtechSwitch, '_set_state', new=AsyncMock(side_effect=mock_set_state)):
|
||||||
|
await switch._set_state(True)
|
||||||
|
|
||||||
|
assert switch.is_on is True
|
Loading…
x
Reference in New Issue
Block a user