Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,29 @@ All notable changes to this project will be documented in this file. Dates are d

Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).

#### [1.3.0](https://github.com/rdkcentral/python_raft/compare/1.2.2...1.3.0)

- Update #132: Created abstract power module and added Tapo support [`#135`](https://github.com/rdkcentral/python_raft/pull/135)
- Release 1.2.2: master -> develop [`#143`](https://github.com/rdkcentral/python_raft/pull/143)
- Fix #132: Fixed typos [`#132`](https://github.com/rdkcentral/python_raft/issues/132)
- Fix #132: Updated example_rack_config to show username password are [`#132`](https://github.com/rdkcentral/python_raft/issues/132)

#### [1.2.2](https://github.com/rdkcentral/python_raft/compare/1.2.1...1.2.2)

> 28 January 2025

- Feature/gh136 ping test output [`#142`](https://github.com/rdkcentral/python_raft/pull/142)
- Release 1.2.1 master -> develop [`#141`](https://github.com/rdkcentral/python_raft/pull/141)
- change self.output in pingTestOnly() with teh actual command output result[0] [`809f674`](https://github.com/rdkcentral/python_raft/commit/809f6742594e6b780a5953f8fa28a20c2411416f)

#### [1.2.1](https://github.com/rdkcentral/python_raft/compare/1.2.0...1.2.1)

> 27 January 2025

- Release 1.2.1 [`#140`](https://github.com/rdkcentral/python_raft/pull/140)
- Release 1.1.2 [`#139`](https://github.com/rdkcentral/python_raft/pull/139)
- Bugfix #137: Corrected telnet usage to follow updated telnet class [`#137`](https://github.com/rdkcentral/python_raft/issues/137)
- Bumped changelog [`b3086d1`](https://github.com/rdkcentral/python_raft/commit/b3086d15bc9b8131334cc78f4f2fb7eb3f4f4b91)
- Merge tag '1.2.0' into develop [`499b9b1`](https://github.com/rdkcentral/python_raft/commit/499b9b1eb8c60531f9617316615b984c92b1780a)

#### [1.2.0](https://github.com/rdkcentral/python_raft/compare/1.1.2...1.2.0)
Expand Down
1 change: 1 addition & 0 deletions examples/configs/example_rack_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ rackConfig:
# [type: "orvbioS20", ip: "", mac: "", port:"optional", relay:"optional"]
# [type: "kasa", ip: "", options:"--plug" ] # <- Plug
# [type: "kasa", ip: "", options:"--strip", args:'--index 2' ] # <- Power Strip
# [type: "tapo", ip: "", username: "", password: "", outlet: "optional"]
# [type: "hs100", ip:"", port:"optional" ] kara also supports hs100
# [type: "apc", ip:"", username:"", password:"" ] rack apc switch
# [type: "olimex", ip:"", port:"optional", relay:"" ]
Expand Down
40 changes: 39 additions & 1 deletion framework/core/powerControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

from framework.core.powerModules.apcAos import powerApcAos
from framework.core.powerModules.kasaControl import powerKasa
from framework.core.powerModules.tapoControl import powerTapo
from framework.core.powerModules.olimex import powerOlimex
from framework.core.powerModules.apc import powerAPC
from framework.core.powerModules.hs100 import powerHS100
Expand Down Expand Up @@ -89,6 +90,8 @@ def __init__(self, log:logModule, config:dict):
self.powerSwitch = powerOlimex( log, self.ip, config.get("port"), config.get("relay"))
elif type == "kasa":
self.powerSwitch = powerKasa( log, **config )
elif type == "tapo":
self.powerSwitch = powerTapo( log, **config)
elif type == "SLP":
self.powerSwitch = powerSLP(log, self.ip, config.get("username"), config.get("password"), config.get("outlet_id"),config.get('port',23))
elif type == "none":
Expand All @@ -112,11 +115,46 @@ def powerOff(self):
self.powerOnState = False
return result


def reboot(self):
self.log.info("reboot")
return self.powerRetry(self.powerSwitch.reboot)

def getPowerLevel(self):
"""Retrieve the current power draw of the device.

Returns:
float: Power draw in Watts.

Raises:
RuntimeError: if powerSwitch type doesn't support power readings.
"""
self.log.debug("Retrieving current Power level")
return self.powerRetry(self.powerSwitch.getPowerLevel)

def getVoltageLevel(self):
"""Retrieve the current Voltage draw of the device.

Returns:
float: Voltage draw in Volts.

Raises:
RuntimeError: if powerSwitch type doesn't support Voltage readings.
"""
self.log.debug("Retrieving current Voltage level")
return self.powerRetry(self.powerSwitch.getVoltageLevel)

def getCurrentLevel(self):
"""Retrieve the current draw of the device.

Returns:
float: Current draw in Amps.

Raises:
RuntimeError: if powerSwitch type doesn't support current readings.
"""
self.log.debug("Retrieving current Voltage level")
return self.powerRetry(self.powerSwitch.getCurrentLevel)

def powerRetry(self, powerMethod):
""" Performs the passed powerMethod and retries it if it fails

Expand Down
121 changes: 121 additions & 0 deletions framework/core/powerModules/abstractPowerModule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
#/* *****************************************************************************
# *
# * If not stated otherwise in this file or this component's LICENSE file the
# * following copyright and licenses apply:
# *
# * Copyright 2023 RDK Management
# *
# * Licensed under the Apache License, Version 2.0 (the "License");
# * you may not use this file except in compliance with the License.
# * You may obtain a copy of the License at
# *
# *
# http://www.apache.org/licenses/LICENSE-2.0
# *
# * Unless required by applicable law or agreed to in writing, software
# * distributed under the License is distributed on an "AS IS" BASIS,
# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# * See the License for the specific language governing permissions and
# * limitations under the License.
# *
#* ******************************************************************************
#*
#* ** Project : RAFT
#* ** @addtogroup : core.powerModules
#* ** @date : 14/01/2025
#* **
#* ** @brief : Abstract Power Module
#* **
#* *****************************************************************************

from abc import ABCMeta, abstractmethod
import time

from framework.core.logModule import logModule


class PowerModuleInterface(metaclass=ABCMeta):

def __init__(cls,logger: logModule):
cls._log = logger
cls._is_on = False

@property
def is_on(cls):
"""Boolean for the current power state of the powerswitch/outlet
True if the powerswitch/outlet is powered on.
"""
return cls._is_on

@property
def is_off(cls):
"""Boolean for the current power state of the powerswitch/outlet
True if the powerswitch/outlet is powered off.
"""
if cls.is_on:
return False
return True

@abstractmethod
def powerOn(cls) -> bool:
"""Turn on the powerswitch/outlet.

Returns:
bool: True if successfully switched on.
"""
pass

@abstractmethod
def powerOff(cls) -> bool:
"""Turn off the powerswitch/outlet.

Returns:
bool: True if successfully switched off.
"""
pass

def reboot(self) -> bool:
"""Power cycle the powerswitch/outlet.

Returns:
bool: True if successfully turned back on.
"""
if self.is_on:
self.powerOff()
time.sleep(1)
self.powerOn()
return self.is_on

def getPowerLevel(cls) -> float:
"""Retrieve the current power draw from the powerswitch/outlet.

Returns:
float: Power level in Watts.

Raises:
RuntimeError: When method cannot be implemented for the powerswitch type.
"""
raise RuntimeError('Power monitoring is not supported by this power module: [{}]'.format(cls.__class__.__name__))

def getVoltageLevel(cls) -> float:
"""Retrieve the current Voltage draw from the powerswitch/outlet.

Returns:
float: Voltage level in Volts.

Raises:
RuntimeError: When method cannot be implemented for the powerswitch type.
"""
raise RuntimeError('Voltage monitoring is not supported by this power module: [{}]'.format(cls.__class__.__name__))

def getCurrentLevel(cls) -> float:
"""Retrieve the current power draw from the powerswitch/outlet.

Returns:
float: Current level in Amps.

Raises:
RuntimeError: When method cannot be implemented for the powerswitch type.
"""
raise RuntimeError('Current monitoring is not supported by this power module: [{}]'.format(cls.__class__.__name__))
29 changes: 15 additions & 14 deletions framework/core/powerModules/kasaControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@
import subprocess

from framework.core.logModule import logModule
from framework.core.powerModules.abstractPowerModule import PowerModuleInterface

class powerKasa():
class powerKasa(PowerModuleInterface):

"""Kasa power switch controller supports
"""
Expand All @@ -81,7 +82,7 @@ def __init__( self, log:logModule, ip:str, args:str=None, options:str=None, **kw
options ([str], optional): [options]. Defaults to None, which translates to "--plug"
kwargs ([dict]): [any other args]
"""
self.log = log
super().__init__(log)
self.is_on = False
self.is_off = False
self.slotIndex=0
Expand All @@ -90,7 +91,7 @@ def __init__( self, log:logModule, ip:str, args:str=None, options:str=None, **kw
#args = config.get("args")
#options = config.get("options")
if options == None:
options = "--plug"
options = "--type plug"
if args == None:
args = ""
else:
Expand Down Expand Up @@ -155,9 +156,9 @@ def performCommand(self, command, noOptions = False, noArgs = False):
extension += " {}".format(self.args)
# kasa [OPTIONS] COMMAND [ARGS]...
command = self.split_with_quotes( "kasa " + extension )
self.log.debug( "Command: {}".format(command))
self._log.debug( "Command: {}".format(command))
data = subprocess.run(command, stdout=subprocess.PIPE, text=True)
self.log.debug(data.stdout)
self._log.debug(data.stdout)
return data.stdout

def powerOff(self):
Expand All @@ -173,7 +174,7 @@ def powerOff(self):
self.performCommand("off")
self.__getstate__()
if self.is_off == False:
self.log.error(" Power Off Failed")
self._log.error(" Power Off Failed")
return self.is_off

def powerOn(self):
Expand All @@ -189,7 +190,7 @@ def powerOn(self):
self.performCommand("on")
self.__getstate__()
if self.is_on == False:
self.log.error(" Power On Failed")
self._log.error(" Power On Failed")
return self.is_on

def __getstate__(self):
Expand All @@ -211,33 +212,33 @@ def __getstate__(self):
elif line[:3] == "OFF":
powerState.append("OFF")
if len(powerState) != 0:
self.log.debug(powerState)
self._log.debug(powerState)
# Check if this strip is off
if powerState[0] == "OFF":
self.is_on = False
self.is_off = True
self.log.debug("Device state: OFF")
self._log.debug("Device state: OFF")
return
# Check if the this socket is off.
if powerState[self.slotIndex+1] == "OFF":
self.is_on = False
self.is_off = True
self.log.debug("Slot state: OFF")
self._log.debug("Slot state: OFF")
else:
self.is_on = True
self.is_off = False
self.log.debug("Slot state: ON")
self._log.debug("Slot state: ON")
else:
result = self.performCommand("state")
# | grep 'Device state' | cut -d ' ' -f 3
if "Device state: OFF" in result:
if "Device state: False" in result:
self.is_on = False
self.is_off = True
self.log.debug("Device state: OFF")
self._log.debug("Device state: OFF")
else:
self.is_on = True
self.is_off = False
self.log.debug("Device state: ON")
self._log.debug("Device state: ON")

def reboot(self):
"""
Expand Down
Loading