Skip to content

Commit

Permalink
Fixed warnings. Moved the coordinator to a separate file. Update the …
Browse files Browse the repository at this point in the history
…switch icon.
  • Loading branch information
slydiman committed Feb 1, 2024
1 parent be43f77 commit dcee4b8
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 149 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ This integration requires using your SSCPOE account `email` and `password`. Use
# Usage

This integration exposes power sensors and POE control switches.

Note: The cloud server does not support multiple connections to the same account from Home Assistant and the mobile app. The device in Home Assistant will disappear after connecting from the official SSCPOE mobile app and will be reconnected within 30 seconds automatically.
133 changes: 2 additions & 131 deletions custom_components/sscpoe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,8 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.exceptions import (
HomeAssistantError,
ConfigEntryAuthFailed,
ConfigEntryNotReady,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import asyncio
import hashlib
import async_timeout
from datetime import timedelta
from .const import DOMAIN, LOGGER
from .protocol import SSCPOE_KEY, SSCPOE_request
from .const import DOMAIN
from .coordinator import SSCPOE_Coordinator

PLATFORMS: list[str] = [Platform.SENSOR, Platform.SWITCH]

Expand All @@ -43,120 +31,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener."""
await hass.config_entries.async_reload(entry.entry_id)


class ApiError(HomeAssistantError):
"""ApiError"""


class ApiAuthError(HomeAssistantError):
"""ApiAuthError"""


class SSCPOE_Coordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, email: str, password: str):
self._email = email
self._password = password
self._key = SSCPOE_KEY
self._uid = None
self.prj = None
self.devices = None

super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)

async def _async_update_data(self) -> None:
try:
async with async_timeout.timeout(10):
return await self.hass.async_add_executor_job(self._fetch_data)
except ApiAuthError as err:
self._uid = None
# Raising ConfigEntryAuthFailed will cancel future updates
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
raise ConfigEntryAuthFailed from err
except ApiError as err:
self._uid = None
raise UpdateFailed(f"Error communicating with API: {err}")

def _fetch_data(self) -> None:
if self._uid is None:
eml = {
"email": self._email,
"pd": hashlib.md5(self._password.encode("utf-8")).hexdigest(),
}
j = SSCPOE_request("eml", eml, SSCPOE_KEY, None)
if j is None:
raise ApiError
if j["errcode"] != 0:
raise ApiAuthError
self._uid = j["uid"]
self._key = j["key"]

if self.devices is None:
if self.prj is None:
j = SSCPOE_request("prjmng", None, self._key, self._uid)
if j is None:
raise ApiError
self.prj = {}
for p in j["admin"]:
pid = p["pid"]
self.prj[pid] = p
j = SSCPOE_request("swmng", {"pid": pid}, self._key, self._uid)
if j is None:
raise ApiError
p["online"] = j["online"]
self.devices = {}
for i, pid in enumerate(self.prj):
p = self.prj[pid]
for s in p["online"]:
sn = s["sn"]
self.devices[sn] = {"pid": pid, "sn": sn}

for i, sn in enumerate(self.devices):
device = self.devices[sn]
j = SSCPOE_request(
"swdet",
{"pid": device["pid"], "sn": sn, "isJoin": "1"},
self._key,
self._uid,
)
if j is None:
raise ApiError
detail = j["detail"]
device["detail"] = detail
if not ("device_info" in device):
device["device_info"] = DeviceInfo(
identifiers={(DOMAIN, sn)},
manufacturer="STEAMEMO",
model=sn[0:6],
name=detail["name"],
sw_version=detail["V"],
connections={
(CONNECTION_NETWORK_MAC, detail["mac"])
}, # ,(CONF_IP_ADDRESS, self._device.detail['ip'])
)

async def _async_switch_poe(self, pid: str, sn: str, port: int, poec: bool) -> None:
try:
async with async_timeout.timeout(10):
return await self.hass.async_add_executor_job(
self._switch_poe, pid, sn, port, poec
)
except ApiError as err:
self._uid = None
raise UpdateFailed(f"Error communicating with API: {err}")

def _switch_poe(self, pid: str, sn: str, port: int, poec: bool) -> None:
if self._uid:
swconf = {
"pid": pid,
"sn": sn,
"opcode": (0x202 if poec else 2) | (port << 4),
}
j = SSCPOE_request("swconf", swconf, self._key, self._uid)
if j is None:
raise ApiError
132 changes: 132 additions & 0 deletions custom_components/sscpoe/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from __future__ import annotations

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
HomeAssistantError,
ConfigEntryAuthFailed,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import hashlib
import async_timeout
from datetime import timedelta
from .const import DOMAIN, LOGGER
from .protocol import SSCPOE_KEY, SSCPOE_request


class SSCPOE_Coordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, email: str, password: str):
self._email = email
self._password = password
self._key = SSCPOE_KEY
self._uid = None
self.prj = None
self.devices = None

super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)

async def _async_update_data(self) -> None:
try:
async with async_timeout.timeout(10):
return await self.hass.async_add_executor_job(self._fetch_data)
except ApiAuthError as err:
self._uid = None
# Raising ConfigEntryAuthFailed will cancel future updates
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
raise ConfigEntryAuthFailed from err
except ApiError as err:
self._uid = None
raise UpdateFailed(f"Error communicating with API: {err}")

def _fetch_data(self) -> None:
if self._uid is None:
eml = {
"email": self._email,
"pd": hashlib.md5(self._password.encode("utf-8")).hexdigest(),
}
j = SSCPOE_request("eml", eml, SSCPOE_KEY, None)
if j is None:
raise ApiError
if j["errcode"] != 0:
raise ApiAuthError(f'errcode={j["errcode"]}')
self._uid = j["uid"]
self._key = j["key"]

if self.devices is None:
if self.prj is None:
j = SSCPOE_request("prjmng", None, self._key, self._uid)
if j is None:
raise ApiError
self.prj = {}
for p in j["admin"]:
pid = p["pid"]
self.prj[pid] = p
j = SSCPOE_request("swmng", {"pid": pid}, self._key, self._uid)
if j is None:
raise ApiError
p["online"] = j["online"]
self.devices = {}
for i, pid in enumerate(self.prj):
p = self.prj[pid]
for s in p["online"]:
sn = s["sn"]
self.devices[sn] = {"pid": pid, "sn": sn}

for i, sn in enumerate(self.devices):
device = self.devices[sn]
j = SSCPOE_request(
"swdet",
{"pid": device["pid"], "sn": sn, "isJoin": "1"},
self._key,
self._uid,
)
if j is None:
raise ApiError
detail = j["detail"]
device["detail"] = detail
if not ("device_info" in device):
device["device_info"] = DeviceInfo(
identifiers={(DOMAIN, sn)},
manufacturer="STEAMEMO",
model=sn[0:6],
name=detail["name"],
sw_version=detail["V"],
connections={
(CONNECTION_NETWORK_MAC, detail["mac"])
}, # ,(CONF_IP_ADDRESS, self._device.detail['ip'])
)

async def _async_switch_poe(self, pid: str, sn: str, port: int, poec: bool) -> None:
try:
async with async_timeout.timeout(10):
return await self.hass.async_add_executor_job(
self._switch_poe, pid, sn, port, poec
)
except ApiError as err:
self._uid = None
raise UpdateFailed(f"Error communicating with API: {err}")

def _switch_poe(self, pid: str, sn: str, port: int, poec: bool) -> None:
if self._uid:
swconf = {
"pid": pid,
"sn": sn,
"opcode": (0x202 if poec else 2) | (port << 4),
}
j = SSCPOE_request("swconf", swconf, self._key, self._uid)
if j is None:
raise ApiError


class ApiError(HomeAssistantError):
"""ApiError"""


class ApiAuthError(HomeAssistantError):
"""ApiAuthError"""
7 changes: 6 additions & 1 deletion custom_components/sscpoe/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,4 +369,9 @@ def SSCPOE_request(act: str, dt, key: str, uid: str):
return j


SSCPOE_errcode = {0: "OK", 20004: "Invalid email", 20004: "Invalid password"}
SSCPOE_errcode = {
0: "OK",
10002: "Multiple login",
20004: "Invalid email",
20004: "Invalid password",
}
23 changes: 9 additions & 14 deletions custom_components/sscpoe/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import POWER_WATT, ELECTRIC_POTENTIAL_VOLT
from homeassistant.const import (
UnitOfElectricPotential,
UnitOfPower,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand All @@ -10,7 +13,7 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, LOGGER
from . import SSCPOE_Coordinator
from .coordinator import SSCPOE_Coordinator


async def async_setup_entry(
Expand All @@ -36,10 +39,10 @@ async def async_setup_entry(


class PortPowerSensor(CoordinatorEntity[SSCPOE_Coordinator], SensorEntity):
_attr_native_unit_of_measurement = POWER_WATT
_attr_unit_of_measurement = POWER_WATT
_attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_device_class = SensorDeviceClass.POWER
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:flash"

def __init__(self, coordinator: SSCPOE_Coordinator, sn: str, port: int):
self._sn = sn
Expand All @@ -59,10 +62,6 @@ def __init__(self, coordinator: SSCPOE_Coordinator, sn: str, port: int):
self.entity_id = f"{DOMAIN}.{sn}_{port+1}_power".lower()
self._attr_device_info = device["device_info"]

@property
def icon(self):
return "mdi:flash"

@callback
def _handle_coordinator_update(self) -> None:
if self._port == -1:
Expand All @@ -81,10 +80,10 @@ async def async_added_to_hass(self) -> None:


class VoltageSensor(CoordinatorEntity[SSCPOE_Coordinator], SensorEntity):
_attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT
_attr_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_attr_device_class = SensorDeviceClass.VOLTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon = "mdi:sine-wave"

def __init__(self, coordinator: SSCPOE_Coordinator, sn: str):
self._sn = sn
Expand All @@ -98,10 +97,6 @@ def __init__(self, coordinator: SSCPOE_Coordinator, sn: str):
self.entity_id = f"{DOMAIN}.{sn}_voltage".lower()
self._attr_device_info = device["device_info"]

@property
def icon(self):
return "mdi:sine-wave"

@callback
def _handle_coordinator_update(self) -> None:
self._attr_native_value = self.coordinator.devices[self._sn]["detail"]["vol"]
Expand Down
6 changes: 3 additions & 3 deletions custom_components/sscpoe/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, LOGGER
from . import SSCPOE_Coordinator
from .coordinator import SSCPOE_Coordinator


async def async_setup_entry(
Expand Down Expand Up @@ -44,9 +44,9 @@ def __init__(self, coordinator: SSCPOE_Coordinator, sn: str, port: int):
@property
def icon(self):
if self.is_on:
return "mdi:toggle-switch-variant" # "mdi:ethernet"
return "mdi:ethernet"
else:
return "mdi:toggle-switch-variant-off" # "mdi:ethernet-off"
return "mdi:ethernet-off"

# @property
# def is_on(self) -> bool:
Expand Down

0 comments on commit dcee4b8

Please sign in to comment.