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

Monitor as switch, linked command with sensor, battery fix #37

Merged
merged 10 commits into from
Jan 26, 2023
11 changes: 8 additions & 3 deletions IoTuring/Entity/Deployments/Battery/Battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ class Battery(Entity):

def Initialize(self):
self.RegisterEntitySensor(EntitySensor(self, KEY_PERCENTAGE))
self.RegisterEntitySensor(EntitySensor(self, KEY_CHARGING_STATUS))

# Check if charging state working:
batteryInfo = self.GetBatteryInformation()
if isinstance(batteryInfo['charging'], bool):
self.RegisterEntitySensor(EntitySensor(self, KEY_CHARGING_STATUS))

def PostInitialize(self):
# Check if battery infomration are present
Expand All @@ -23,8 +27,9 @@ def Update(self):
batteryInfo = self.GetBatteryInformation()
self.SetEntitySensorValue(KEY_PERCENTAGE, int(
batteryInfo['level']), ValueFormatter.Options(ValueFormatter.TYPE_PERCENTAGE))
self.SetEntitySensorValue(
KEY_CHARGING_STATUS, str(batteryInfo['charging']))
if isinstance(batteryInfo['charging'], bool):
self.SetEntitySensorValue(
KEY_CHARGING_STATUS, str(batteryInfo['charging']))

def GetBatteryInformation(self):
battery = psutil.sensors_battery()
Expand Down
75 changes: 52 additions & 23 deletions IoTuring/Entity/Deployments/Monitor/Monitor.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import subprocess
import ctypes
import os as sys_os
import os
import re

from IoTuring.Entity.Entity import Entity
from ctypes import *
from IoTuring.Entity.EntityData import EntityCommand, EntitySensor
from IoTuring.Entity import consts
from IoTuring.Logger.consts import STATE_OFF, STATE_ON

from IoTuring.Entity.EntityData import EntityCommand

KEY_TURN_ALL_OFF = 'turn_all_off'
KEY_TURN_ALL_ON = 'turn_all_on'
KEY_STATE = 'monitor_state'
KEY_CMD = 'monitor'


class Monitor(Entity):
Expand All @@ -20,26 +23,52 @@ def Initialize(self):
def PostInitialize(self):
self.os = self.GetDependentEntitySensorValue('Os', "operating_system")

if self.os == 'Windows' or (self.os == 'Linux' and sys_os.environ.get('DISPLAY')):
if self.os == consts.OS_FIXED_VALUE_LINUX:
# Check if xset is working:
p = subprocess.run(
['xset', 'dpms'], capture_output=True, shell=False)
if p.stderr:
raise Exception(f"Xset dpms error: {p.stderr.decode()}")
elif not os.getenv('DISPLAY'):
raise Exception('No $DISPLAY environment variable!')
else:
supports_linux = True

if self.os == consts.OS_FIXED_VALUE_WINDOWS:
self.RegisterEntityCommand(EntityCommand(
self, KEY_TURN_ALL_OFF, self.CallbackTurnAllOff))
self, KEY_CMD, self.Callback))
elif supports_linux:
# Support for sending state on linux
self.RegisterEntitySensor(EntitySensor(self, KEY_STATE))
self.RegisterEntityCommand(EntityCommand(
self, KEY_TURN_ALL_ON, self.CallbackTurnAllOn))

def CallbackTurnAllOff(self, message):
if self.os == 'Windows':
ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, 2)
elif self.os == 'Linux':
# Check if X11 or something else
if sys_os.environ.get('DISPLAY'):
command = 'xset dpms force off'
subprocess.Popen(command.split(), stdout=subprocess.PIPE)
self, KEY_CMD, self.Callback, KEY_STATE))

def Callback(self, message):
payloadString = message.payload.decode('utf-8')

def CallbackTurnAllOn(self, message):
if self.os == 'Windows':
ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, -1)
elif self.os == 'Linux':
# Check if X11 or something else
if sys_os.environ.get('DISPLAY'):
if payloadString == STATE_ON:
if self.os == consts.OS_FIXED_VALUE_WINDOWS:
ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, -1)
elif self.os == consts.OS_FIXED_VALUE_LINUX:
command = 'xset dpms force on'
subprocess.Popen(command.split(), stdout=subprocess.PIPE)

elif payloadString == STATE_OFF:
if self.os == consts.OS_FIXED_VALUE_WINDOWS:
ctypes.windll.user32.SendMessageA(0xFFFF, 0x0112, 0xF170, 2)
elif self.os == consts.OS_FIXED_VALUE_LINUX:
command = 'xset dpms force off'
subprocess.Popen(command.split(), stdout=subprocess.PIPE)
else:
raise Exception('Incorrect payload!')

def Update(self):
if self.os == consts.OS_FIXED_VALUE_LINUX:
p = subprocess.run(['xset', 'q'], capture_output=True, shell=False)
outputString = p.stdout.decode()
monitorState = re.findall(
'Monitor is (.{2,3})', outputString)[0].upper()
if monitorState in [STATE_OFF, STATE_ON]:
self.SetEntitySensorValue(KEY, monitorState)
else:
raise Exception(f'Incorrect monitor state: {monitorState}')
18 changes: 17 additions & 1 deletion IoTuring/Entity/EntityData.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,25 @@ def SetExtraAttributes(self, _dict):

class EntityCommand(EntityData):

def __init__(self, entity, key, callbackFunction):
def __init__(self, entity, key, callbackFunction, connectedEntitySensorKey = None):
"""
If a key for the entity sensor is passed, warehouses that support it use this command as a switch with state.
Better to register the sensor before this command to avoud unexpected behaviours.
"""
EntityData.__init__(self, entity, key)
self.callbackFunction = callbackFunction
self.connectedEntitySensorKey = connectedEntitySensorKey

def SupportsState(self):
return self.connectedEntitySensorKey is not None

def GetConnectedEntitySensorKey(self):
# if this support state, return the key of the entity sensor that is connected to this command
# otherwise return None
if self.SupportsState():
return self.connectedEntitySensorKey
else:
return None

def CallCallback(self, message):
""" Safely run callback for this command, passing the message (a paho.mqtt.client.MQTTMessage) """
Expand Down
4 changes: 4 additions & 0 deletions IoTuring/Logger/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
LOG_DEVELOPMENT = 5


# On/off states as strings:
STATE_ON = "ON"
STATE_OFF = "OFF"

# Fill start of string with spaces to jusitfy the message (0: no padding)
# First for type, second for source
STRINGS_LENGTH = [8, 30]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from IoTuring.Protocols.MQTTClient.MQTTClient import MQTTClient
from IoTuring.Warehouse.Warehouse import Warehouse
from IoTuring.MyApp.App import App
from IoTuring.Logger import consts

import json
import yaml
import re
Expand Down Expand Up @@ -31,8 +33,10 @@
EXTERNAL_ENTITY_DATA_CONFIGURATION_KEY_CUSTOM_TYPE = "custom_type"

LWT_TOPIC_SUFFIX = "LWT"
LWT_PAYLOAD_ONLINE = "ON"
LWT_PAYLOAD_OFFLINE = "OFF"
LWT_PAYLOAD_ONLINE = "ONLINE"
LWT_PAYLOAD_OFFLINE = "OFFLINE"
PAYLOAD_ON = consts.STATE_ON
PAYLOAD_OFF = consts.STATE_OFF


class HomeAssistantWarehouse(Warehouse):
Expand Down Expand Up @@ -103,17 +107,29 @@ def SendSensorsValues(self):
def SendEntityDataConfigurations(self):
self.SendLwtSensorConfiguration()
for entity in self.GetEntities():

# Get sensors entity data linked to commands so they are not configured as sensor but
# will be set in command state which will be a switch
keys_of_sensors_connected_to_commands = [command.GetConnectedEntitySensorKey() for command in entity.GetEntityCommands() if command.SupportsState()]

for entityData in entity.GetAllEntityData():

# if I found a sensor data that will be configured only after, together with
# its command (as a switch)
if entityData.GetKey() in keys_of_sensors_connected_to_commands:
continue # it's a sensor linked to a command, so skip

data_type = ""
autoDiscoverySendTopic = ""
payload = {}
payload['name'] = entity.GetEntityNameWithTag() + " - " + \
entityData.GetKey()

# check the data type: can be edited from custom configurations
if entityData in entity.GetEntitySensors(): # it's an EntitySensorData
data_type = "sensor"
else: # it's a EntityCommandData
elif entityData.SupportsState(): # it's a EntityCommandData: has it a state ?
data_type = "switch"
else:
data_type = "button"

# add custom info to the entity data, reading it from external file and accessing the information using the entity data name
Expand All @@ -132,23 +148,38 @@ def SendEntityDataConfigurations(self):
payload['unique_id'] = self.clientName + \
"." + entityData.GetId()

if entityData in entity.GetEntitySensors(): # it's an EntitySensorData
# add configurations about sensors or switches (both have a state)
if entityData in entity.GetEntitySensors() or data_type=='switch': # it's an EntitySensorData
# If the sensor supports extra attributes, send them as JSON.
# So here I have to specify also the topic for those attrbitues
if entityData.DoesSupportExtraAttributes():
# So here I have to specify also the topic for those attributes
# (not supported by switches)
if data_type!='switch' and entityData.DoesSupportExtraAttributes():
richibrics marked this conversation as resolved.
Show resolved Hide resolved
payload["json_attributes_topic"] = self.MakeEntityDataExtraAttributesTopic(
entityData)

payload['expire_after'] = 600 # TODO Improve
payload['state_topic'] = self.MakeEntityDataTopic(
entityData)
autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format(
data_type, App.getName(), payload['unique_id'].replace(".", "_"))
else: # it's a EntityCommandData


if data_type!='switch':
payload['state_topic'] = self.MakeEntityDataTopic(
entityData)
else: # it's a switch, so the state is the state of the connected sensor
payload['state_topic'] = self.MakeEntityDataTopic(entity.GetEntitySensorByKey(entityData.GetConnectedEntitySensorKey()))
infeeeee marked this conversation as resolved.
Show resolved Hide resolved

# Add default payloads (only for ON/OFF entities)
if data_type in ["binary_sensor", "switch"]:
if not 'payload_on' in payload:
payload['payload_on'] = PAYLOAD_ON
if not 'payload_off' in payload:
payload['payload_off'] = PAYLOAD_OFF

# if it's a command (so button or switch), configure the topic where the command will be called
if entityData in entity.GetEntityCommands():
payload['command_topic'] = self.MakeEntityDataTopic(
entityData)
autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format(
data_type, App.getName(), payload['unique_id'].replace(".", "_"))

autoDiscoverySendTopic = TOPIC_AUTODISCOVERY_FORMAT.format(
data_type, App.getName(), payload['unique_id'].replace(".", "_"))

# Add availability configuration
payload["availability_topic"] = self.MakeValuesTopic(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,8 @@ Cpu: # if no matches with above CPU patterns, set this
icon: "mdi:calculator-variant"
Time:
icon: "mdi:clock"
Monitor - turn_all_on:
icon: "mdi:monitor-star"
Monitor - turn_all_off:
icon: "mdi:monitor-off"
Monitor:
icon: "mdi:monitor-shimmer"
AppInfo:
icon: "mdi:information-outline"
Temperature:
Expand Down