move repo to single repo

This commit is contained in:
Niklas 2025-01-03 22:01:49 +01:00
commit 5eb4a9b9f3
15 changed files with 1044 additions and 0 deletions

300
.gitignore vendored Normal file
View File

@ -0,0 +1,300 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# PyPI configuration file
.pypirc
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
repomix-output.xml
app.db
.ha

0
README.md Normal file
View File

14
compose.yml Normal file
View File

@ -0,0 +1,14 @@
services:
has:
image: ghcr.io/home-assistant/home-assistant:stable
restart: unless-stopped
ports:
- "8123:8123"
volumes:
- ./custom_components/smart_home:/config/custom_components/smart_home
- .ha/config:/config
- /etc/localtime:/etc/localtime:ro
- /run/dbus:/run/dbus:ro
environment:
- TZ=Europe/Berlin

View File

@ -0,0 +1,63 @@
"""The Smart Home integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform, CONF_HOST, CONF_PORT, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from .const import (
DOMAIN,
DATA_COORDINATOR,
DATA_CONFIG,
DEFAULT_PORT,
DEVICE_TYPE_MAPPING,
)
from .coordinator import SmartHomeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [
Platform.SWITCH,
Platform.BUTTON,
Platform.COVER,
Platform.SENSOR,
Platform.BINARY_SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Smart Home from a config entry."""
hass.data.setdefault(DOMAIN, {})
coordinator = SmartHomeDataUpdateCoordinator(
hass,
config=entry.data,
entry_id=entry.entry_id,
)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = {
DATA_COORDINATOR: coordinator,
DATA_CONFIG: entry.data,
}
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."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator: SmartHomeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
await coordinator.async_shutdown()
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,39 @@
"""Support for Smart Home binary sensors."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import SmartHomeDataUpdateCoordinator
from .entity import SmartHomeEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Smart Home binary sensors."""
coordinator: SmartHomeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
entities = []
for device_id, device in coordinator._devices.items():
if device["device_type"] == "binary_sensor":
entities.append(SmartHomeBinarySensor(coordinator, device_id))
async_add_entities(entities)
class SmartHomeBinarySensor(SmartHomeEntity, BinarySensorEntity):
"""Representation of a Smart Home binary sensor."""
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.device_state.get("state", False)

View File

@ -0,0 +1,43 @@
"""Support for Smart Home pushbuttons."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import SmartHomeDataUpdateCoordinator
from .entity import SmartHomeEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Smart Home buttons."""
coordinator: SmartHomeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
entities = []
#for device_id, device in coordinator._devices.items():
# if device["device_type"] == "pushbutton":
# entities.append(SmartHomeButton(coordinator, device_id))
async_add_entities(entities)
class SmartHomeButton(SmartHomeEntity, ButtonEntity):
"""Representation of a Smart Home button."""
async def async_press(self) -> None:
"""Press the button."""
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"state": True}) as response:
if response.status != 200:
_LOGGER.error("Failed to press button: %s", response.status)

View File

@ -0,0 +1,59 @@
"""Config flow for Smart Home integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
import aiohttp
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, DEFAULT_PORT, STEP_USER
_LOGGER = logging.getLogger(__name__)
class SmartHomeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Smart Home."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
# Test connection to API
async with aiohttp.ClientSession() as session:
url = f"http://{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
async with session.get(f"{url}/") as response:
if response.status == 200:
return self.async_create_entry(
title=user_input[CONF_NAME],
data=user_input,
)
else:
errors["base"] = "cannot_connect"
except aiohttp.ClientError:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return self.async_show_form(
step_id=STEP_USER,
data_schema=vol.Schema(
{
vol.Required(CONF_NAME): str,
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
),
errors=errors,
)

View File

@ -0,0 +1,25 @@
"""Constants for the Smart Home integration."""
from typing import Final
DOMAIN: Final = "smart_home"
CONF_HOST: Final = "host"
CONF_PORT: Final = "port"
CONF_NAME: Final = "name"
DEFAULT_PORT: Final = 5000
# Config flow and options flow
STEP_USER: Final = "user"
DATA_COORDINATOR: Final = "coordinator"
DATA_CONFIG: Final = "config"
# Device type mapping
DEVICE_TYPE_MAPPING = {
"switch": "switch",
"pushbutton": "button",
"blind": "cover",
"sensor": "sensor",
"binary_sensor": "binary_sensor"
}

View File

@ -0,0 +1,133 @@
"""DataUpdateCoordinator for Smart Home integration."""
from __future__ import annotations
import asyncio
import json
import logging
from datetime import timedelta
from typing import Any
import aiohttp
import websocket
import threading
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import CONF_HOST, CONF_PORT
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class SmartHomeDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(
self,
hass: HomeAssistant,
*,
config: dict[str, Any],
entry_id: str,
) -> None:
"""Initialize."""
self.config = config
self.api_url = f"http://{config[CONF_HOST]}:{config[CONF_PORT]}"
self.ws_url = f"ws://{config[CONF_HOST]}:{config[CONF_PORT]}/ws"
self.ws = None
self.ws_thread = None
self.entry_id = entry_id
self._devices = {}
self._device_states = {}
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=30), # Fallback update interval
)
self._start_ws_client()
async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.api_url}/api/devices") as response:
if response.status != 200:
raise UpdateFailed(f"Error communicating with API: {response.status}")
devices = await response.json()
# Update internal device cache
new_devices = {}
for device in devices:
if not device["enabled"]:
continue
new_devices[str(device["id"])] = device
self._devices = new_devices
return self._devices
except aiohttp.ClientError as error:
raise UpdateFailed(f"Error communicating with API: {error}")
@callback
def async_device_state_update(self, msg: dict[str, Any]) -> None:
"""Process device state update from WebSocket."""
if msg["type"] == "device_update":
device_id = str(msg["device_id"])
if device_id in self._devices:
self._device_states[device_id] = msg["state"]
self.async_set_updated_data(self._devices)
elif msg["type"] == "initial_states":
for state in msg["states"]:
device_id = str(state["device_id"])
self._device_states[device_id] = state["state"]
self.async_set_updated_data(self._devices)
def _ws_connect(self) -> None:
"""Connect to WebSocket in a separate thread."""
_LOGGER.info("Connecting to WebSocket")
websocket.enableTrace(True)
self.ws = websocket.WebSocketApp(
self.ws_url,
on_message=self._ws_message,
on_error=self._ws_error,
on_close=self._ws_close,
)
self.ws.run_forever()
def _ws_message(self, _, message: str) -> None:
"""Handle incoming WebSocket message."""
try:
msg = json.loads(message)
self.hass.add_job(self.async_device_state_update, msg)
except json.JSONDecodeError:
_LOGGER.error("Failed to parse WebSocket message")
def _ws_error(self, _, error: Any) -> None:
"""Handle WebSocket error."""
_LOGGER.error("WebSocket error: %s", error)
def _ws_close(self, *args: Any) -> None:
"""Handle WebSocket close."""
_LOGGER.warning("WebSocket connection closed")
# Attempt to reconnect after a delay
self.hass.loop.call_later(30, self._start_ws_client)
def _start_ws_client(self) -> None:
"""Start WebSocket client in a separate thread."""
if self.ws_thread is not None and self.ws_thread.is_alive():
_LOGGER.warning("WebSocket client already running")
return
_LOGGER.info("Starting WebSocket client")
self.ws_thread = threading.Thread(target=self._ws_connect, daemon=True)
self.ws_thread.start()
async def async_shutdown(self) -> None:
"""Shutdown the coordinator."""
if self.ws:
self.ws.close()

View File

@ -0,0 +1,137 @@
"""Support for Smart Home blinds."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import SmartHomeDataUpdateCoordinator
from .entity import SmartHomeEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Smart Home covers."""
coordinator: SmartHomeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
entities = []
for device_id, device in coordinator._devices.items():
if device["device_type"] == "blind":
entities.append(SmartHomeCover(coordinator, device_id))
async_add_entities(entities)
class SmartHomeCover(SmartHomeEntity, CoverEntity):
"""Representation of a Smart Home cover."""
def __init__(
self,
coordinator: SmartHomeDataUpdateCoordinator,
device_id: str,
) -> None:
"""Initialize the cover."""
super().__init__(coordinator, device_id)
# Set supported features based on position control capability
if self.device_data.get("can_use_positions", False):
self._attr_supported_features = CoverEntityFeature.SET_POSITION
else:
self._attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
if self.device_data.get("can_use_positions", False):
await self._async_set_cover_position(100)
else:
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"position": 100}) as response:
if response.status != 200:
_LOGGER.error("Failed to open cover: %s", response.status)
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
if self.device_data.get("can_use_positions", False):
await self._async_set_cover_position(0)
else:
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"position": 0}) as response:
if response.status != 200:
_LOGGER.error("Failed to close cover: %s", response.status)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"position": -1}) as response:
if response.status != 200:
_LOGGER.error("Failed to stop cover: %s", response.status)
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
if not self.device_data.get("can_use_positions", False):
_LOGGER.warning("Trying to set position for a blind that doesn't support position control")
return
position = kwargs.get("position", 0)
await self._async_set_cover_position(position)
async def _async_set_cover_position(self, position: int) -> None:
"""Helper to set cover position."""
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"position": position}) as response:
if response.status != 200:
_LOGGER.error("Failed to set cover position: %s", response.status)
@property
def current_cover_position(self) -> int | None:
"""Return current position of cover."""
position = self.device_state.get("position")
if position is None or position == -1:
return None
return position
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed or not."""
position = self.current_cover_position
if position is None:
return None
return position == 0
@property
def is_opening(self) -> bool:
"""Return if the cover is opening."""
if self.device_data.get("moving", False):
if self.device_data.get("can_use_positions", False):
return self.device_state.get("position", 0) > self.device_data.get("current_position", 0)
else:
# For non-position blinds, check if moving up
return self.device_state.get("position", 0) == 100
return False
@property
def is_closing(self) -> bool:
"""Return if the cover is closing."""
if self.device_data.get("moving", False):
if self.device_data.get("can_use_positions", False):
return self.device_state.get("position", 0) < self.device_data.get("current_position", 0)
else:
# For non-position blinds, check if moving down
return self.device_state.get("position", 0) == 0
return False

View File

@ -0,0 +1,63 @@
"""Base entity for Smart Home integration."""
from __future__ import annotations
import logging
from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import SmartHomeDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class SmartHomeEntity(CoordinatorEntity):
"""Base class for Smart Home entities."""
def __init__(
self,
coordinator: SmartHomeDataUpdateCoordinator,
device_id: str,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._device_id = device_id
self._attr_device_info = {
"identifiers": {(DOMAIN, device_id)},
"name": self.device_data["name"],
"manufacturer": "casaIT",
"model": self.device_data["device_type"],
"via_device": (DOMAIN, coordinator.entry_id),
}
@property
def device_data(self) -> dict:
"""Get device data."""
return self.coordinator._devices[self._device_id]
@property
def device_state(self) -> dict:
"""Get device state."""
return self.coordinator._device_states.get(self._device_id, {})
@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.last_update_success and self._device_id in self.coordinator._devices
@property
def name(self):
"""Name of the sensor"""
return self.device_data["name"]
@property
def unique_id(self):
"""ID of the sensor"""
return f'casait_{self.device_data["id"]}_{self.device_data["uuid"]}'
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self.async_write_ha_state()

View File

@ -0,0 +1,11 @@
{
"domain": "smart_home",
"name": "casaIT SmartHome",
"documentation": "https://github.com/yourusername/smart_home",
"dependencies": [],
"codeowners": ["@Gurkengewuerz"],
"requirements": ["websocket-client>=1.8.0"],
"config_flow": true,
"iot_class": "local_push",
"version": "1.0.0"
}

View File

@ -0,0 +1,96 @@
"""Support for Smart Home sensors."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
SensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import SmartHomeDataUpdateCoordinator
from .entity import SmartHomeEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Smart Home sensors."""
coordinator: SmartHomeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
entities = []
for device_id, device in coordinator._devices.items():
if device["device_type"] == "sensor":
if device.get("onewire_conversion_type") == "DS2438TEMP":
entities.append(SmartHomeTemperatureSensor(coordinator, device_id))
elif device.get("onewire_type") == "DS18XB20":
entities.append(SmartHomeTemperatureSensor(coordinator, device_id))
elif device.get("onewire_conversion_type") in ["HIH4030", "HIH5030"]:
entities.append(SmartHomeHumiditySensor(coordinator, device_id))
elif device.get("onewire_conversion_type") == "TEPT5600":
entities.append(SmartHomeLightSensor(coordinator, device_id))
else:
entities.append(SmartHomeGenericSensor(coordinator, device_id))
async_add_entities(entities)
class SmartHomeTemperatureSensor(SmartHomeEntity, SensorEntity):
"""Representation of a Smart Home temperature sensor."""
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = "°C"
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
return self.device_state.get("value")
class SmartHomeHumiditySensor(SmartHomeEntity, SensorEntity):
"""Representation of a Smart Home humidity sensor."""
_attr_device_class = SensorDeviceClass.HUMIDITY
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = "%"
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
return self.device_state.get("value")
class SmartHomeLightSensor(SmartHomeEntity, SensorEntity):
"""Representation of a Smart Home light sensor."""
_attr_device_class = SensorDeviceClass.ILLUMINANCE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = "lx"
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
_LOGGER.debug(f"Light sensor value: {self.device_state}")
return self.device_state.get("value")
class SmartHomeGenericSensor(SmartHomeEntity, SensorEntity):
"""Representation of a Smart Home generic sensor."""
_attr_state_class = SensorStateClass.MEASUREMENT
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
return self.device_state.get("value")
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
return self.device_state.get("unit")

View File

@ -0,0 +1,56 @@
"""Support for Smart Home switches."""
from __future__ import annotations
import logging
from typing import Any
import aiohttp
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DATA_COORDINATOR
from .coordinator import SmartHomeDataUpdateCoordinator
from .entity import SmartHomeEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Smart Home switches."""
coordinator: SmartHomeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
entities = []
for device_id, device in coordinator._devices.items():
if device["device_type"] == "switch" or device["device_type"] == "pushbutton":
entities.append(SmartHomeSwitch(coordinator, device_id))
async_add_entities(entities)
class SmartHomeSwitch(SmartHomeEntity, SwitchEntity):
"""Representation of a Smart Home switch."""
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"state": True}) as response:
if response.status != 200:
_LOGGER.error("Failed to turn on switch: %s", response.status)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
async with aiohttp.ClientSession() as session:
url = f"{self.coordinator.api_url}/api/devices/{self._device_id}/state"
async with session.put(url, json={"state": False}) as response:
if response.status != 200:
_LOGGER.error("Failed to turn off switch: %s", response.status)
@property
def is_on(self) -> bool | None:
"""Return true if switch is on."""
return self.device_state.get("state", False)

5
hacs.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "casaIT SmartHome",
"render_readme": true,
"homeassistant": "2024.9.0"
}