Skip to content

Commit 617554f

Browse files
committed
2 parents fff3107 + c4f545e commit 617554f

9 files changed

Lines changed: 507 additions & 16 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
.vscode
1+
.vscode
2+
HIDDEN
3+
OLD
4+
*__pycache__

__init__.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""The Detailed Hello World Push integration."""
2+
from __future__ import annotations
3+
4+
from homeassistant.config_entries import ConfigEntry
5+
from homeassistant.core import HomeAssistant
6+
from homeassistant.helpers.typing import ConfigType
7+
8+
from . import hub
9+
from .const import DOMAIN
10+
11+
VERSION = "0.0.1"
12+
13+
# List of platforms to support. There should be a matching .py file for each,
14+
# eg <cover.py> and <sensor.py>
15+
PLATFORMS: list[str] = ["sensor"]
16+
17+
18+
# async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
19+
# """Set up a skeleton component."""
20+
# hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub.Hub(hass, None)
21+
# hass.config_entries.async_setup_platforms(entry, PLATFORMS)
22+
23+
# return True
24+
25+
26+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
27+
"""Set up Hello World from a config entry."""
28+
# Store an instance of the "connecting" class that does the work of speaking
29+
# with your actual devices.
30+
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub.Hub(hass, entry.data["host"])
31+
32+
# This creates each HA object for each platform your device requires.
33+
# It's done by calling the `async_setup_entry` function in each platform module.
34+
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
35+
return True
36+
37+
38+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
39+
"""Unload a config entry."""
40+
# This is called when an entry/configured device is to be removed. The class
41+
# needs to unload itself, and remove callbacks. See the classes for further
42+
# details
43+
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
44+
if unload_ok:
45+
hass.data[DOMAIN].pop(entry.entry_id)
46+
47+
return unload_ok

config_flow.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Config flow for Hello World integration."""
2+
from __future__ import annotations
3+
4+
import logging
5+
from typing import Any
6+
7+
import voluptuous as vol
8+
9+
from homeassistant import config_entries, exceptions
10+
from homeassistant.core import HomeAssistant
11+
12+
from .const import DOMAIN # pylint:disable=unused-import
13+
from .hub import Hub
14+
15+
_LOGGER = logging.getLogger(__name__)
16+
17+
# This is the schema that used to display the UI to the user. This simple
18+
# schema has a single required host field, but it could include a number of fields
19+
# such as username, password etc. See other components in the HA core code for
20+
# further examples.
21+
# Note the input displayed to the user will be translated. See the
22+
# translations/<lang>.json file and strings.json. See here for further information:
23+
# https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#translations
24+
# At the time of writing I found the translations created by the scaffold didn't
25+
# quite work as documented and always gave me the "Lokalise key references" string
26+
# (in square brackets), rather than the actual translated value. I did not attempt to
27+
# figure this out or look further into it.
28+
DATA_SCHEMA = vol.Schema({("host"): str})
29+
# DATA_SCHEMA = vol.Schema({})
30+
31+
32+
async def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]:
33+
"""Validate the user input allows us to connect.
34+
35+
Data has the keys from DATA_SCHEMA with values provided by the user.
36+
"""
37+
# Validate the data can be used to set up a connection.
38+
39+
# This is a simple example to show an error in the UI for a short hostname
40+
# The exceptions are defined at the end of this file, and are used in the
41+
# `async_step_user` method below.
42+
if len(data["host"]) < 3:
43+
raise InvalidHost
44+
45+
hub = Hub(hass, data["host"])
46+
# The dummy hub provides a `test_connection` method to ensure it's working
47+
# as expected
48+
result = await hub.test_connection()
49+
if not result:
50+
# If there is an error, raise an exception to notify HA that there was a
51+
# problem. The UI will also show there was a problem
52+
raise CannotConnect
53+
54+
# If your PyPI package is not built with async, pass your methods
55+
# to the executor:
56+
# await hass.async_add_executor_job(
57+
# your_validate_func, data["username"], data["password"]
58+
# )
59+
60+
# If you cannot connect:
61+
# throw CannotConnect
62+
# If the authentication is wrong:
63+
# InvalidAuth
64+
65+
# Return info that you want to store in the config entry.
66+
# "Title" is what is displayed to the user for this hub device
67+
# It is stored internally in HA as part of the device config.
68+
# See `async_step_user` below for how this is used
69+
return {"title": data["host"]}
70+
71+
72+
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
73+
"""Handle a config flow for Hello World."""
74+
75+
VERSION = 1
76+
# Pick one of the available connection classes in homeassistant/config_entries.py
77+
# This tells HA if it should be asking for updates, or it'll be notified of updates
78+
# automatically. This example uses PUSH, as the dummy hub will notify HA of
79+
# changes.
80+
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
81+
82+
async def async_step_user(self, user_input=None):
83+
"""Handle the initial step."""
84+
# This goes through the steps to take the user through the setup process.
85+
# Using this it is possible to update the UI and prompt for additional
86+
# information. This example provides a single form (built from `DATA_SCHEMA`),
87+
# and when that has some validated input, it calls `async_create_entry` to
88+
# actually create the HA config entry. Note the "title" value is returned by
89+
# `validate_input` above.
90+
errors = {}
91+
if user_input is not None:
92+
try:
93+
info = await validate_input(self.hass, user_input)
94+
95+
return self.async_create_entry(title=info["title"], data=user_input)
96+
except CannotConnect:
97+
errors["base"] = "cannot_connect"
98+
except InvalidHost:
99+
# The error string is set here, and should be translated.
100+
# This example does not currently cover translations, see the
101+
# comments on `DATA_SCHEMA` for further details.
102+
# Set the error on the `host` field, not the entire form.
103+
errors["host"] = "cannot_connect"
104+
except Exception: # pylint: disable=broad-except
105+
_LOGGER.exception("Unexpected exception")
106+
errors["base"] = "unknown"
107+
108+
# If there is no user input or there were errors, show the form again, including any errors that were found with the input.
109+
return self.async_show_form(
110+
step_id="user", data_schema=DATA_SCHEMA, errors=errors
111+
)
112+
113+
114+
class CannotConnect(exceptions.HomeAssistantError):
115+
"""Error to indicate we cannot connect."""
116+
117+
118+
class InvalidHost(exceptions.HomeAssistantError):
119+
"""Error to indicate there is an invalid hostname."""

const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DOMAIN = "ph803w"

hub.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""A demonstration 'hub' that connects several devices."""
2+
from __future__ import annotations
3+
4+
# In a real implementation, this would be in an external library that's on PyPI.
5+
# The PyPI package needs to be included in the `requirements` section of manifest.json
6+
# See https://developers.home-assistant.io/docs/creating_integration_manifest
7+
# for more information.
8+
# This dummy hub always returns 3 rollers.
9+
import asyncio
10+
import socket
11+
import random
12+
13+
from .lib import discovery, device
14+
15+
from homeassistant.core import HomeAssistant
16+
17+
PH803W_DEFAULT_TCP_PORT = 12416
18+
19+
20+
class Hub:
21+
"""Dummy hub for Hello World example."""
22+
23+
manufacturer = "Demonstration Corp"
24+
25+
def __init__(self, hass: HomeAssistant, host: str) -> None:
26+
"""Init dummy hub."""
27+
# discover = discovery.Discovery()
28+
# res = discover.run()
29+
dev = device.Device(host)
30+
res = dev.run()
31+
# dev.close()
32+
33+
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
34+
self.socket.connect((host, PH803W_DEFAULT_TCP_PORT))
35+
36+
data = bytes.fromhex("0000000303000006")
37+
self.socket.sendall(data)
38+
response = self.socket.recv(1024)
39+
passcode_lenth = response[9]
40+
passcode_raw = response[10 : 10 + passcode_lenth]
41+
passcode = passcode_raw.decode("utf-8")
42+
43+
data = (
44+
bytes.fromhex("000000030f00000800")
45+
+ passcode_lenth.to_bytes(1, "little")
46+
+ passcode_raw
47+
)
48+
self.socket.sendall(data)
49+
response = self.socket.recv(1024)
50+
if response[8] != 0:
51+
print("Error connecting")
52+
53+
# Connection established, from now on some cyclig bahavior
54+
data = bytes.fromhex("000000030400009002")
55+
self.socket.sendall(data)
56+
empty_counter = 0
57+
data = bytes.fromhex("0000000303000015")
58+
response = self.socket.recv(1024)
59+
# if len(response) == 0:
60+
# empty_counter += 1
61+
# continue
62+
empty_counter = 0
63+
# print(response)
64+
if len(response) == 18:
65+
flag1 = response[8]
66+
if flag1 & 0b0000_0100:
67+
print("In water")
68+
flag2 = response[9]
69+
if flag2 & 0b0000_0010:
70+
print("ORP on")
71+
if flag2 & 0b0000_0001:
72+
print("PH on")
73+
# state_raw = response[8 : 9]
74+
ph_raw = response[10:12]
75+
self.ph = int.from_bytes(ph_raw, "big") * 0.01
76+
redox_raw = response[12:14]
77+
self.redox = int.from_bytes(redox_raw, "big") - 2000
78+
unknown1_raw = response[14:16]
79+
unknown1 = int.from_bytes(unknown1_raw, "big")
80+
unknown2_raw = response[15:18]
81+
unknown2 = int.from_bytes(unknown2_raw, "big")
82+
83+
self._host = host
84+
self._hass = hass
85+
self._name = host
86+
self._id = host.lower()
87+
self.probe = Probe(f"{self._id}_1", f"{self._name} 1", self)
88+
self.online = True
89+
90+
@property
91+
def hub_id(self) -> str:
92+
"""ID for dummy hub."""
93+
return self._id
94+
95+
async def test_connection(self) -> bool:
96+
"""Test connectivity to the Dummy hub is OK."""
97+
await asyncio.sleep(1)
98+
return True
99+
100+
101+
class Probe:
102+
"""Dummy roller (device for HA) for Hello World example."""
103+
104+
def __init__(self, id: str, name: str, hub: Hub) -> None:
105+
"""Init dummy roller."""
106+
self._id = id
107+
self.hub = hub
108+
self.name = name
109+
self._ph_value = hub.ph
110+
self._orp_value = hub.redox
111+
self._callbacks = set()
112+
self._loop = asyncio.get_event_loop()
113+
114+
# Some static information about this device
115+
self.firmware_version = f"0.0.{random.randint(1, 9)}"
116+
self.model = "Test Device"
117+
118+
@property
119+
def id(self) -> str:
120+
"""Return ID for roller."""
121+
return self._id
122+
123+
# async def set_position(self, position: int) -> None:
124+
# """
125+
# Set dummy cover to the given position.
126+
127+
# State is announced a random number of seconds later.
128+
# """
129+
# self._target_position = position
130+
131+
# # Update the moving status, and broadcast the update
132+
# self.moving = position - 50
133+
# await self.publish_updates()
134+
135+
# self._loop.create_task(self.delayed_update())
136+
137+
async def delayed_update(self) -> None:
138+
"""Publish updates, with a random delay to emulate interaction with device."""
139+
await asyncio.sleep(random.randint(1, 10))
140+
self.moving = 0
141+
await self.publish_updates()
142+
143+
def register_callback(self, callback: Callable[[], None]) -> None:
144+
"""Register callback, called when Roller changes state."""
145+
self._callbacks.add(callback)
146+
147+
def remove_callback(self, callback: Callable[[], None]) -> None:
148+
"""Remove previously registered callback."""
149+
self._callbacks.discard(callback)
150+
151+
# In a real implementation, this library would call it's call backs when it was
152+
# notified of any state changeds for the relevant device.
153+
async def publish_updates(self) -> None:
154+
"""Schedule call all registered callbacks."""
155+
pass
156+
# self._current_position = self._target_position
157+
# for callback in self._callbacks:
158+
# callback()
159+
160+
@property
161+
def online(self) -> float:
162+
"""Roller is online."""
163+
# The dummy roller is offline about 10% of the time. Returns True if online,
164+
# False if offline.
165+
return random.random() > 0.1
166+
167+
@property
168+
def ph(self) -> float:
169+
"""Battery level as a percentage."""
170+
return self._ph_value
171+
172+
@property
173+
def orp(self) -> float:
174+
"""Return a random voltage roughly that of a 12v battery."""
175+
return self._orp_value

0 commit comments

Comments
 (0)