Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop 1.1.0 #146

Merged
merged 11 commits into from
Oct 9, 2023
Empty file modified .github/ISSUE_TEMPLATE/bug_report.md
100644 → 100755
Empty file.
Empty file modified .github/ISSUE_TEMPLATE/feature_request.md
100644 → 100755
Empty file.
Empty file modified .github/workflows/testsuite.yml
100644 → 100755
Empty file.
Empty file modified .gitignore
100644 → 100755
Empty file.
Empty file modified LICENSE
100644 → 100755
Empty file.
Empty file modified README.md
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion ShellyMQTT.indigoPlugin/Contents/Info.plist
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>PluginVersion</key>
<string>1.0.2</string>
<string>1.1.0</string>
<key>ServerApiVersion</key>
<string>3.0</string>
<key>IwsApiVersion</key>
Expand Down
Empty file modified ShellyMQTT.indigoPlugin/Contents/Resources/icon.png
100644 → 100755
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified ShellyMQTT.indigoPlugin/Contents/Server Plugin/Actions.xml
100644 → 100755
Empty file.
24 changes: 24 additions & 0 deletions ShellyMQTT.indigoPlugin/Contents/Server Plugin/Devices.xml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,30 @@
<Option value="C">Celsius</Option>
</List>
</Field>

<Field type="menu" id="heating-status" defaultValue="valve">
<Label>Heating Status:</Label>
<List>
<Option value="valve">Valve Position</Option>
<Option value="temperature">Temperature</Option>
</List>
</Field>

<Field id="heating-status-valve-threshold" type="textfield" defaultValue="5" visibleBindingId="heating-status" visibleBindingValue="valve">
<Label>Minimum Valve Position:</Label>
</Field>

<Field id="heating-status-valve-help" type="label" fontSize="small" fontColor="darkGrey" visibleBindingId="heating-status" visibleBindingValue="valve">
<Label>The TRV will be considered to be "heating" when the valve position is greater than the threshold position.</Label>
</Field>

<Field id="heating-status-temperature-threshold" type="textfield" defaultValue="0.5" visibleBindingId="heating-status" visibleBindingValue="temperature">
<Label>Temperature Threshold:</Label>
</Field>

<Field id="heating-status-temperature-help" type="label" fontSize="small" fontColor="darkGrey" visibleBindingId="heating-status" visibleBindingValue="temperature">
<Label>The TRV will be considered to be "heating" when the temperature is less than the threshold from the setpoint.</Label>
</Field>
</ConfigUI>
<States>
<State id="online">
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions ShellyMQTT.indigoPlugin/Contents/Server Plugin/Devices/Shelly.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,22 @@ def parseAnnouncement(self, payload):
self.device.updateStateOnServer('firmware-version', firmware_version)
self.device.updateStateOnServer('has-firmware-update', has_firmware_update)

if firmware_version:
# Populate the firmware UI column
cleaned_firmware_version: str = firmware_version
if "/" in cleaned_firmware_version:
# Remove the date part of the version string
cleaned_firmware_version = cleaned_firmware_version.split("/")[-1]
if cleaned_firmware_version[0] == "v":
# Remove the leading "v"
cleaned_firmware_version = cleaned_firmware_version[1:]
# Replace "@" with "-" for consistency
cleaned_firmware_version = cleaned_firmware_version.replace("@", "-")

pluginProps = self.device.pluginProps
pluginProps.update({"version": cleaned_firmware_version})
self.device.replacePluginPropsOnServer(pluginProps)

def processInputEvent(self, eventMessage):
"""
Parses an input event message and fires triggers if this is a new input event.
Expand Down
Empty file.
Empty file.
46 changes: 45 additions & 1 deletion ShellyMQTT.indigoPlugin/Contents/Server Plugin/Devices/Shelly_TRV.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class Shelly_TRV(Shelly):
def __init__(self, device):
Shelly.__init__(self, device)
self.device.updateStateImageOnServer(indigo.kStateImageSel.HvacHeating)
self.updateStateImage()
self.device.updateStateOnServer("hvacOperationMode", value=indigo.kHvacMode.Heat)
self.device.updateStateOnServer("hvacHeaterIsOn", value=True)

Expand Down Expand Up @@ -51,6 +51,8 @@ def handleMessage(self, topic, payload):
self.updateCharger(charger)
self.updateCalibrated(calibrated)
self.processThermostat(thermostat)

self.updateStateImage()
elif topic == "{}/settings".format(self.getAddress()):
payload = json.loads(payload)
thermostats = payload.get("thermostats", [])
Expand Down Expand Up @@ -292,3 +294,45 @@ def get_schedule_profiles(self):
names[index + 1] = name

return names

def updateStateImage(self):
"""
Updates the state image for the device.

The state image is selected based on the state of the device as well as
the state image mode.

The two modes are: `Valve Position` and `Temperature`

In the `Valve Position` mode, the device is considered to be heating
when the valve position is greater than a specified position threshold.
This lets the user set a minimum position that should indicate if the
valve is heating.

In the `Temperature` mode, the device is considered to be heating when
the observed temperature is below a specified threshold of the setpoint.

Since the TRV will only ever heat an area, the icons `HvacHeatMode` and
`HvacHeating` are used to represent an idle state and a heating state
respectively. The idle state icon is an unfilled grey flame, and the
heating icon is a colored flame.
"""
heating_status = self.device.pluginProps.get('heating-status')
if heating_status == "valve":
threshold = self.device.pluginProps.get('heating-status-valve-threshold')
if threshold is None:
return

if self.device.states['valve-position'] > int(threshold):
self.device.updateStateImageOnServer(indigo.kStateImageSel.HvacHeating)
else:
self.device.updateStateImageOnServer(indigo.kStateImageSel.HvacHeatMode)
elif heating_status == "temperature":
threshold = self.device.pluginProps.get('heating-status-temperature-threshold')
if threshold is None:
return

if self.device.states['temperatureInput1'] < (self.device.states["setpointHeat"] - float(threshold)):
self.device.updateStateImageOnServer(indigo.kStateImageSel.HvacHeating)
else:
self.device.updateStateImageOnServer(indigo.kStateImageSel.HvacHeatMode)
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
1 change: 1 addition & 0 deletions ShellyMQTT.indigoPlugin/Contents/Server Plugin/Devices/tests/mocking/IndigoServer.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(self):
self.EnergyMeterOn = "EnergyMeterOn"
self.EnergyMeterOff = "EnergyMeterOff"
self.HvacHeating = "HvacHeating"
self.HvacHeatMode = "HvacHeatMode"


class HVACMode:
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions ShellyMQTT.indigoPlugin/Contents/Server Plugin/Devices/tests/test_Shelly.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,15 @@ def test_parseAnnouncement_invalid_announcement_for_device(self):
self.assertEqual("3", self.device.states['firmware-version'])
self.assertFalse(self.device.states['has-firmware-update'])

def test_parseAnnouncement_formats_version_for_display(self):
self.device.pluginProps['address'] = "shellies/test-shelly"
self.shelly.parseAnnouncement('{"id": "test-shelly", "fw_ver": "20221027-100516/1.12.1-ga9117d3"}')
self.assertEqual(self.device.pluginProps["version"], "1.12.1-ga9117d3")
self.shelly.parseAnnouncement('{"id": "test-shelly", "fw_ver": "20221027-100516/v1.12.1-ga9117d3"}')
self.assertEqual(self.device.pluginProps["version"], "1.12.1-ga9117d3")
self.shelly.parseAnnouncement('{"id": "test-shelly", "fw_ver": "20220620-083944/v2.1.6@166b8318+"}')
self.assertEqual(self.device.pluginProps["version"], "2.1.6-166b8318+")

def test_updateEnergy_4_decimals(self):
self.shelly.updateEnergy(50)
self.assertAlmostEqual(0.0008, self.shelly.device.states['accumEnergyTotal'], 4)
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
41 changes: 40 additions & 1 deletion ShellyMQTT.indigoPlugin/Contents/Server Plugin/Devices/tests/test_Shelly_TRV.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def test_trv_default_schedule_profile_names(self):
self.assertEqual(expected, self.shelly.get_schedule_profiles())

@patch('Devices.Shelly.Shelly.publish')
def test_handlePluginAction_set_schedule_profile_and_enable(self, publish):
def test_handlePluginAction_set_schedule_profile_and_disable(self, publish):
set_schedule_profile = PluginAction("trv-set-schedule-profile")
set_schedule_profile.props = {"schedule-profile": "1", "enable-schedule": False}
self.shelly.handlePluginAction(set_schedule_profile)
Expand All @@ -225,3 +225,42 @@ def test_handlePluginAction_disable_schedule(self, publish):
disable_schedule = PluginAction("trv-disable-schedule")
self.shelly.handlePluginAction(disable_schedule)
publish.assert_called_with("shellies/trv/thermostat/0/command/schedule", "0")

def test_update_state_image_idle_below_valve_threshold(self):
"""Test the icon shows idle when the valve position is below the threshold"""
self.device.pluginProps['heating-status'] = "valve"
self.device.pluginProps['heating-status-valve-threshold'] = "5"
self.device.states['valve-position'] = 2
self.shelly.updateStateImage()
assert self.device.image == indigo.kStateImageSel.HvacHeatMode

def test_update_state_image_heating_above_valve_threshold(self):
"""Test the icon shows heating when the valve position is below the threshold"""
self.device.pluginProps['heating-status'] = "valve"
self.device.pluginProps['heating-status-valve-threshold'] = "5"
self.device.states['valve-position'] = 33
self.shelly.updateStateImage()
assert self.device.image == indigo.kStateImageSel.HvacHeating

def test_update_state_image_idle_below_temperature_threshold(self):
"""Test the icon shows idle when the valve position is below the threshold"""
self.device.pluginProps['heating-status'] = "temperature"
self.device.pluginProps['heating-status-temperature-threshold'] = "0.5"
self.device.states["setpointHeat"] = 23
self.device.states['temperatureInput1'] = 22.8
self.shelly.updateStateImage()
assert self.device.image == indigo.kStateImageSel.HvacHeatMode

def test_update_state_image_heating_above_temperature_threshold(self):
"""Test the icon shows heating when the valve position is below the threshold"""
self.device.pluginProps['heating-status'] = "temperature"
self.device.pluginProps['heating-status-temperature-threshold'] = "0.5"
self.device.states["setpointHeat"] = 23
self.device.states['temperatureInput1'] = 22.2
self.shelly.updateStateImage()
assert self.device.image == indigo.kStateImageSel.HvacHeating

@patch('Devices.Shelly_TRV.Shelly_TRV.updateStateImage')
def test_info_message_updates_icon(self, updateStateImage):
self.shelly.handleMessage("shellies/trv/info", '{}')
updateStateImage.assert_called_once()
Empty file.
Empty file.
Empty file.
Empty file modified ShellyMQTT.indigoPlugin/Contents/Server Plugin/Events.xml
100644 → 100755
Empty file.
Empty file modified ShellyMQTT.indigoPlugin/Contents/Server Plugin/MenuItems.xml
100644 → 100755
Empty file.
Empty file modified ShellyMQTT.indigoPlugin/Contents/Server Plugin/PluginConfig.xml
100644 → 100755
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file modified ShellyMQTT.indigoPlugin/Contents/Server Plugin/build_code.txt
100644 → 100755
Empty file.
21 changes: 21 additions & 0 deletions ShellyMQTT.indigoPlugin/Contents/Server Plugin/plugin.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,23 @@ def processAnnouncement(self, brokerId, payload):
This method will check against a list of known devices and will keep track
of announcement messages that don't belong to a known device.

Gen 2 devices will send an announcement that looks like:
{
"name":null,
"id":"shellyplugus-c049ef88c33c",
"mac":"C049EF88C33C",
"slot":0,
"model":"SNPL-00116US",
"gen":2,
"fw_id":"20230912-081939/1.0.3-g6176478",
"ver":"1.0.3",
"app":"PlugUS",
"auth_en":false,
"auth_domain":null
}

Gen 2 messages should be ignored.

:param brokerId The device id of the broker that the message was published to.
:param payload The payload of the message.
:return: None
Expand All @@ -817,6 +834,10 @@ def processAnnouncement(self, brokerId, payload):
self.logger.error(u"Unable to convert '{}' into python object!".format(payload))
return

if announcement.get("gen", 1) == 2:
self.logger.debug(f"Ignoring gen 2 device announcement from {announcement.get('id', 'Unknown')}")
return

# Ensure we at least have the id key present
identifier = announcement.get('id', None)
if not identifier:
Expand Down