Merge branch 'main' of 91.92.155.224:Innovenergy/Innovenergy_trunk
This commit is contained in:
commit
8710a1c8ce
|
@ -0,0 +1,54 @@
|
|||
import serial
|
||||
import logging
|
||||
from data import read_file_one_line
|
||||
|
||||
# dbus configuration
|
||||
|
||||
CONNECTION = 'Modbus RTU'
|
||||
PRODUCT_NAME = 'FIAMM 48TL Series Battery'
|
||||
PRODUCT_ID = 0xB012 # assigned by victron
|
||||
DEVICE_INSTANCE = 1
|
||||
SERVICE_NAME_PREFIX = 'com.victronenergy.battery.'
|
||||
|
||||
|
||||
# driver configuration
|
||||
|
||||
SOFTWARE_VERSION = '3.0.0'
|
||||
UPDATE_INTERVAL = 2000 # milliseconds
|
||||
LOG_LEVEL = logging.INFO
|
||||
#LOG_LEVEL = logging.DEBUG
|
||||
|
||||
|
||||
# battery config
|
||||
|
||||
V_MAX = 54.2
|
||||
V_MIN = 42
|
||||
R_STRING_MIN = 0.125
|
||||
R_STRING_MAX = 0.250
|
||||
I_MAX_PER_STRING = 15
|
||||
AH_PER_STRING = 40
|
||||
|
||||
# modbus configuration
|
||||
|
||||
BASE_ADDRESS = 999
|
||||
NO_OF_REGISTERS = 56
|
||||
MAX_SLAVE_ADDRESS = 25
|
||||
|
||||
|
||||
# RS 485 configuration
|
||||
|
||||
PARITY = serial.PARITY_ODD
|
||||
TIMEOUT = 0.1 # seconds
|
||||
BAUD_RATE = 115200
|
||||
BYTE_SIZE = 8
|
||||
STOP_BITS = 1
|
||||
MODE = 'rtu'
|
||||
|
||||
# InnovEnergy IOT configuration
|
||||
|
||||
INSTALLATION_NAME = read_file_one_line('/data/innovenergy/openvpn/installation-name')
|
||||
INNOVENERGY_SERVER_IP = '10.2.0.1'
|
||||
INNOVENERGY_SERVER_PORT = 8134
|
||||
INNOVENERGY_PROTOCOL_VERSION = '48TL200V3'
|
||||
|
||||
|
Binary file not shown.
|
@ -0,0 +1,160 @@
|
|||
import struct
|
||||
|
||||
import config as cfg
|
||||
from data import LedState, BatteryStatus
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, List, Iterable, Union, AnyStr, Any
|
||||
|
||||
|
||||
def read_bool(base_register, bit):
|
||||
# type: (int, int) -> Callable[[BatteryStatus], bool]
|
||||
|
||||
# TODO: explain base register offset
|
||||
register = base_register + int(bit/16)
|
||||
bit = bit % 16
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> bool
|
||||
value = status.modbus_data[register - cfg.BASE_ADDRESS]
|
||||
return value & (1 << bit) > 0
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_float(register, scale_factor=1.0, offset=0.0):
|
||||
# type: (int, float, float) -> Callable[[BatteryStatus], float]
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> float
|
||||
value = status.modbus_data[register - cfg.BASE_ADDRESS]
|
||||
|
||||
if value >= 0x8000: # convert to signed int16
|
||||
value -= 0x10000 # fiamm stores their integers signed AND with sign-offset @#%^&!
|
||||
|
||||
return (value + offset) * scale_factor
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_registers(register, count):
|
||||
# type: (int, int) -> Callable[[BatteryStatus], List[int]]
|
||||
|
||||
start = register - cfg.BASE_ADDRESS
|
||||
end = start + count
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> List[int]
|
||||
return [x for x in status.modbus_data[start:end]]
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def comma_separated(values):
|
||||
# type: (Iterable[str]) -> str
|
||||
return ", ".join(set(values))
|
||||
|
||||
|
||||
def count_bits(base_register, nb_of_registers, nb_of_bits, first_bit=0):
|
||||
# type: (int, int, int, int) -> Callable[[BatteryStatus], int]
|
||||
|
||||
get_registers = read_registers(base_register, nb_of_registers)
|
||||
end_bit = first_bit + nb_of_bits
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> int
|
||||
|
||||
registers = get_registers(status)
|
||||
bin_registers = [bin(x)[-1:1:-1] for x in registers] # reverse the bits in each register so that bit0 is to the left
|
||||
str_registers = [str(x).ljust(16, "0") for x in bin_registers] # add leading zeroes, so all registers are 16 chars long
|
||||
bit_string = ''.join(str_registers) # join them, one long string of 0s and 1s
|
||||
filtered_bits = bit_string[first_bit:end_bit] # take the first nb_of_bits bits starting at first_bit
|
||||
|
||||
return filtered_bits.count('1') # count 1s
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_led_state(register, led):
|
||||
# type: (int, int) -> Callable[[BatteryStatus], int]
|
||||
|
||||
read_lo = read_bool(register, led * 2)
|
||||
read_hi = read_bool(register, led * 2 + 1)
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> int
|
||||
|
||||
lo = read_lo(status)
|
||||
hi = read_hi(status)
|
||||
|
||||
if hi:
|
||||
if lo:
|
||||
return LedState.blinking_fast
|
||||
else:
|
||||
return LedState.blinking_slow
|
||||
else:
|
||||
if lo:
|
||||
return LedState.on
|
||||
else:
|
||||
return LedState.off
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
def unit(unit):
|
||||
# type: (unicode) -> Callable[[unicode], unicode]
|
||||
|
||||
def get_text(v):
|
||||
# type: (unicode) -> unicode
|
||||
return "{0}{1}".format(str(v), unit)
|
||||
|
||||
return get_text
|
||||
|
||||
|
||||
def const(constant):
|
||||
# type: (any) -> Callable[[any], any]
|
||||
def get(*args):
|
||||
return constant
|
||||
return get
|
||||
|
||||
|
||||
def mean(numbers):
|
||||
# type: (List[Union[float,int]]) -> float
|
||||
return float(sum(numbers)) / len(numbers)
|
||||
|
||||
|
||||
def first(ts, default=None):
|
||||
return next((t for t in ts), default)
|
||||
|
||||
|
||||
def bitfields_to_str(lists):
|
||||
# type: (List[List[int]]) -> str
|
||||
|
||||
def or_lists():
|
||||
# type: () -> Iterable[int]
|
||||
|
||||
length = len(first(lists))
|
||||
n_lists = len(lists)
|
||||
|
||||
for i in range(0, length):
|
||||
e = 0
|
||||
for l in range(0, n_lists):
|
||||
e = e | lists[l][i]
|
||||
yield e
|
||||
|
||||
hexed = [
|
||||
'{0:0>4X}'.format(x)
|
||||
for x in or_lists()
|
||||
]
|
||||
|
||||
return ' '.join(hexed)
|
||||
|
||||
|
||||
def pack_string(string):
|
||||
# type: (AnyStr) -> Any
|
||||
data = string.encode('UTF-8')
|
||||
return struct.pack('B', len(data)) + data
|
||||
|
Binary file not shown.
|
@ -0,0 +1,125 @@
|
|||
import config as cfg
|
||||
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, List, Optional, AnyStr, Union, Any
|
||||
|
||||
|
||||
class LedState(object):
|
||||
"""
|
||||
from page 6 of the '48TLxxx ModBus Protocol doc'
|
||||
"""
|
||||
off = 0
|
||||
on = 1
|
||||
blinking_slow = 2
|
||||
blinking_fast = 3
|
||||
|
||||
|
||||
class LedColor(object):
|
||||
green = 0
|
||||
amber = 1
|
||||
blue = 2
|
||||
red = 3
|
||||
|
||||
|
||||
class ServiceSignal(object):
|
||||
|
||||
def __init__(self, dbus_path, get_value_or_const, unit=''):
|
||||
# type: (str, Union[Callable[[],Any],Any], Optional[AnyStr] )->None
|
||||
|
||||
self.get_value_or_const = get_value_or_const
|
||||
self.dbus_path = dbus_path
|
||||
self.unit = unit
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
try:
|
||||
return self.get_value_or_const() # callable
|
||||
except:
|
||||
return self.get_value_or_const # value
|
||||
|
||||
|
||||
class BatterySignal(object):
|
||||
|
||||
def __init__(self, dbus_path, aggregate, get_value, unit=''):
|
||||
# type: (str, Callable[[List[any]],any], Callable[[BatteryStatus],any], Optional[AnyStr] )->None
|
||||
"""
|
||||
A Signal holds all information necessary for the handling of a
|
||||
certain datum (e.g. voltage) published by the battery.
|
||||
|
||||
:param dbus_path: str
|
||||
object_path on DBus where the datum needs to be published
|
||||
|
||||
:param aggregate: Iterable[any] -> any
|
||||
function that combines the values of multiple batteries into one.
|
||||
e.g. sum for currents, or mean for voltages
|
||||
|
||||
:param get_value: (BatteryStatus) -> any
|
||||
function to extract the datum from the modbus record,
|
||||
"""
|
||||
|
||||
self.dbus_path = dbus_path
|
||||
self.aggregate = aggregate
|
||||
self.get_value = get_value
|
||||
self.unit = unit
|
||||
|
||||
|
||||
class Battery(object):
|
||||
|
||||
""" Data record to hold hardware and firmware specs of the battery """
|
||||
|
||||
def __init__(self, slave_address, hardware_version, firmware_version, bms_version, ampere_hours):
|
||||
# type: (int, str, str, str, int) -> None
|
||||
self.slave_address = slave_address
|
||||
self.hardware_version = hardware_version
|
||||
self.firmware_version = firmware_version
|
||||
self.bms_version = bms_version
|
||||
self.ampere_hours = ampere_hours
|
||||
self.n_strings = int(ampere_hours/cfg.AH_PER_STRING)
|
||||
self.i_max = self.n_strings * cfg.I_MAX_PER_STRING
|
||||
self.v_min = cfg.V_MIN
|
||||
self.v_max = cfg.V_MAX
|
||||
self.r_int_min = cfg.R_STRING_MIN / self.n_strings
|
||||
self.r_int_max = cfg.R_STRING_MAX / self.n_strings
|
||||
|
||||
def __str__(self):
|
||||
return 'slave address = {0}\nhardware version = {1}\nfirmware version = {2}\nbms version = {3}\nampere hours = {4}'.format(
|
||||
self.slave_address, self.hardware_version, self.firmware_version, self.bms_version, str(self.ampere_hours))
|
||||
|
||||
|
||||
class BatteryStatus(object):
|
||||
"""
|
||||
record holding the current status of a battery
|
||||
"""
|
||||
def __init__(self, battery, modbus_data):
|
||||
# type: (Battery, List[int]) -> None
|
||||
|
||||
self.battery = battery
|
||||
self.modbus_data = modbus_data
|
||||
|
||||
def serialize(self):
|
||||
# type: () -> str
|
||||
|
||||
b = self.battery
|
||||
|
||||
s = cfg.INNOVENERGY_PROTOCOL_VERSION + '\n'
|
||||
s += cfg.INSTALLATION_NAME + '\n'
|
||||
s += str(b.slave_address) + '\n'
|
||||
s += b.hardware_version + '\n'
|
||||
s += b.firmware_version + '\n'
|
||||
s += b.bms_version + '\n'
|
||||
s += str(b.ampere_hours) + '\n'
|
||||
|
||||
for d in self.modbus_data:
|
||||
s += str(d) + '\n'
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def read_file_one_line(file_name):
|
||||
|
||||
with open(file_name, 'r') as file:
|
||||
return file.read().replace('\n', '').replace('\r', '').strip()
|
||||
|
Binary file not shown.
|
@ -0,0 +1,354 @@
|
|||
#!/usr/bin/python2 -u
|
||||
# coding=utf-8
|
||||
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import gobject
|
||||
import signals
|
||||
import config as cfg
|
||||
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
from pymodbus.client.sync import ModbusSerialClient as Modbus
|
||||
from pymodbus.exceptions import ModbusException, ModbusIOException
|
||||
from pymodbus.other_message import ReportSlaveIdRequest
|
||||
from pymodbus.pdu import ExceptionResponse
|
||||
from pymodbus.register_read_message import ReadInputRegistersResponse
|
||||
from data import BatteryStatus, BatterySignal, Battery, ServiceSignal
|
||||
from python_libs.ie_dbus.dbus_service import DBusService
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, List, Iterable, NoReturn
|
||||
|
||||
|
||||
RESET_REGISTER = 0x2087
|
||||
|
||||
|
||||
def init_modbus(tty):
|
||||
# type: (str) -> Modbus
|
||||
|
||||
logging.debug('initializing Modbus')
|
||||
|
||||
return Modbus(
|
||||
port='/dev/' + tty,
|
||||
method=cfg.MODE,
|
||||
baudrate=cfg.BAUD_RATE,
|
||||
stopbits=cfg.STOP_BITS,
|
||||
bytesize=cfg.BYTE_SIZE,
|
||||
timeout=cfg.TIMEOUT,
|
||||
parity=cfg.PARITY)
|
||||
|
||||
|
||||
def init_udp_socket():
|
||||
# type: () -> socket
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setblocking(False)
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def report_slave_id(modbus, slave_address):
|
||||
# type: (Modbus, int) -> str
|
||||
|
||||
slave = str(slave_address)
|
||||
|
||||
logging.debug('requesting slave id from node ' + slave)
|
||||
|
||||
with modbus:
|
||||
|
||||
request = ReportSlaveIdRequest(unit=slave_address)
|
||||
response = modbus.execute(request)
|
||||
|
||||
if response is ExceptionResponse or issubclass(type(response), ModbusException):
|
||||
raise Exception('failed to get slave id from ' + slave + ' : ' + str(response))
|
||||
|
||||
return response.identifier
|
||||
|
||||
|
||||
def identify_battery(modbus, slave_address):
|
||||
# type: (Modbus, int) -> Battery
|
||||
|
||||
logging.info('identifying battery...')
|
||||
|
||||
hardware_version, bms_version, ampere_hours = parse_slave_id(modbus, slave_address)
|
||||
firmware_version = read_firmware_version(modbus, slave_address)
|
||||
|
||||
specs = Battery(
|
||||
slave_address=slave_address,
|
||||
hardware_version=hardware_version,
|
||||
firmware_version=firmware_version,
|
||||
bms_version=bms_version,
|
||||
ampere_hours=ampere_hours)
|
||||
|
||||
logging.info('battery identified:\n{0}'.format(str(specs)))
|
||||
|
||||
return specs
|
||||
|
||||
|
||||
def identify_batteries(modbus):
|
||||
# type: (Modbus) -> List[Battery]
|
||||
|
||||
def _identify_batteries():
|
||||
slave_address = 0
|
||||
n_missing = -255
|
||||
|
||||
while n_missing < 3:
|
||||
slave_address += 1
|
||||
try:
|
||||
yield identify_battery(modbus, slave_address)
|
||||
n_missing = 0
|
||||
except Exception as e:
|
||||
logging.info('failed to identify battery at {0} : {1}'.format(str(slave_address), str(e)))
|
||||
n_missing += 1
|
||||
|
||||
logging.info('giving up searching for further batteries')
|
||||
|
||||
batteries = list(_identify_batteries()) # dont be lazy!
|
||||
|
||||
n = len(batteries)
|
||||
logging.info('found ' + str(n) + (' battery' if n == 1 else ' batteries'))
|
||||
|
||||
return batteries
|
||||
|
||||
|
||||
def parse_slave_id(modbus, slave_address):
|
||||
# type: (Modbus, int) -> (str, str, int)
|
||||
|
||||
slave_id = report_slave_id(modbus, slave_address)
|
||||
|
||||
sid = re.sub(r'[^\x20-\x7E]', '', slave_id) # remove weird special chars
|
||||
|
||||
match = re.match('(?P<hw>48TL(?P<ah>[0-9]+)) *(?P<bms>.*)', sid)
|
||||
|
||||
if match is None:
|
||||
raise Exception('no known battery found')
|
||||
|
||||
return match.group('hw').strip(), match.group('bms').strip(), int(match.group('ah').strip())
|
||||
|
||||
|
||||
def read_firmware_version(modbus, slave_address):
|
||||
# type: (Modbus, int) -> str
|
||||
|
||||
logging.debug('reading firmware version')
|
||||
|
||||
with modbus:
|
||||
|
||||
response = read_modbus_registers(modbus, slave_address, base_address=1054, count=1)
|
||||
register = response.registers[0]
|
||||
|
||||
return '{0:0>4X}'.format(register)
|
||||
|
||||
|
||||
def read_modbus_registers(modbus, slave_address, base_address=cfg.BASE_ADDRESS, count=cfg.NO_OF_REGISTERS):
|
||||
# type: (Modbus, int, int, int) -> ReadInputRegistersResponse
|
||||
|
||||
logging.debug('requesting modbus registers {0}-{1}'.format(base_address, base_address + count))
|
||||
|
||||
return modbus.read_input_registers(
|
||||
address=base_address,
|
||||
count=count,
|
||||
unit=slave_address)
|
||||
|
||||
|
||||
def read_battery_status(modbus, battery):
|
||||
# type: (Modbus, Battery) -> BatteryStatus
|
||||
"""
|
||||
Read the modbus registers containing the battery's status info.
|
||||
"""
|
||||
|
||||
logging.debug('reading battery status')
|
||||
|
||||
with modbus:
|
||||
data = read_modbus_registers(modbus, battery.slave_address)
|
||||
return BatteryStatus(battery, data.registers)
|
||||
|
||||
|
||||
def publish_values_on_dbus(service, battery_signals, battery_statuses):
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
|
||||
publish_individuals(service, battery_signals, battery_statuses)
|
||||
publish_aggregates(service, battery_signals, battery_statuses)
|
||||
|
||||
|
||||
def publish_aggregates(service, signals, battery_statuses):
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
|
||||
for s in signals:
|
||||
if s.aggregate is None:
|
||||
continue
|
||||
values = [s.get_value(battery_status) for battery_status in battery_statuses]
|
||||
value = s.aggregate(values)
|
||||
service.own_properties.set(s.dbus_path, value, s.unit)
|
||||
|
||||
|
||||
def publish_individuals(service, signals, battery_statuses):
|
||||
# type: (DBusService, Iterable[BatterySignal], Iterable[BatteryStatus]) -> ()
|
||||
|
||||
for signal in signals:
|
||||
for battery_status in battery_statuses:
|
||||
address = battery_status.battery.slave_address
|
||||
dbus_path = '/_Battery/' + str(address) + signal.dbus_path
|
||||
value = signal.get_value(battery_status)
|
||||
service.own_properties.set(dbus_path, value, signal.unit)
|
||||
|
||||
|
||||
def publish_service_signals(service, signals):
|
||||
# type: (DBusService, Iterable[ServiceSignal]) -> NoReturn
|
||||
|
||||
for signal in signals:
|
||||
service.own_properties.set(signal.dbus_path, signal.value, signal.unit)
|
||||
|
||||
|
||||
def upload_status_to_innovenergy(sock, statuses):
|
||||
# type: (socket, Iterable[BatteryStatus]) -> bool
|
||||
|
||||
logging.debug('upload status')
|
||||
|
||||
try:
|
||||
for s in statuses:
|
||||
sock.sendto(s.serialize(), (cfg.INNOVENERGY_SERVER_IP, cfg.INNOVENERGY_SERVER_PORT))
|
||||
except:
|
||||
logging.debug('FAILED')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def print_usage():
|
||||
print ('Usage: ' + __file__ + ' <serial device>')
|
||||
print ('Example: ' + __file__ + ' ttyUSB0')
|
||||
|
||||
|
||||
def parse_cmdline_args(argv):
|
||||
# type: (List[str]) -> str
|
||||
|
||||
if len(argv) == 0:
|
||||
logging.info('missing command line argument for tty device')
|
||||
print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
return argv[0]
|
||||
|
||||
|
||||
def reset_batteries(modbus, batteries):
|
||||
# type: (Modbus, Iterable[Battery]) -> NoReturn
|
||||
|
||||
logging.info('Resetting batteries...')
|
||||
|
||||
for battery in batteries:
|
||||
|
||||
result = modbus.write_registers(RESET_REGISTER, [1], unit=battery.slave_address)
|
||||
|
||||
# expecting a ModbusIOException (timeout)
|
||||
# BMS can no longer reply because it is already reset
|
||||
success = isinstance(result, ModbusIOException)
|
||||
|
||||
outcome = 'successfully' if success else 'FAILED to'
|
||||
logging.info('Battery {0} {1} reset'.format(str(battery.slave_address), outcome))
|
||||
|
||||
logging.info('Shutting down fz-sonick driver')
|
||||
exit(0)
|
||||
|
||||
|
||||
alive = True # global alive flag, watchdog_task clears it, update_task sets it
|
||||
|
||||
|
||||
def create_update_task(modbus, service, batteries):
|
||||
# type: (Modbus, DBusService, Iterable[Battery]) -> Callable[[],bool]
|
||||
"""
|
||||
Creates an update task which runs the main update function
|
||||
and resets the alive flag
|
||||
"""
|
||||
_socket = init_udp_socket()
|
||||
_signals = signals.init_battery_signals()
|
||||
|
||||
def update_task():
|
||||
# type: () -> bool
|
||||
|
||||
global alive
|
||||
|
||||
logging.debug('starting update cycle')
|
||||
|
||||
if service.own_properties.get('/ResetBatteries').value == 1:
|
||||
reset_batteries(modbus, batteries)
|
||||
|
||||
statuses = [read_battery_status(modbus, battery) for battery in batteries]
|
||||
|
||||
publish_values_on_dbus(service, _signals, statuses)
|
||||
upload_status_to_innovenergy(_socket, statuses)
|
||||
|
||||
logging.debug('finished update cycle\n')
|
||||
|
||||
alive = True
|
||||
|
||||
return True
|
||||
|
||||
return update_task
|
||||
|
||||
|
||||
def create_watchdog_task(main_loop):
|
||||
# type: (DBusGMainLoop) -> Callable[[],bool]
|
||||
"""
|
||||
Creates a Watchdog task that monitors the alive flag.
|
||||
The watchdog kills the main loop if the alive flag is not periodically reset by the update task.
|
||||
Who watches the watchdog?
|
||||
"""
|
||||
def watchdog_task():
|
||||
# type: () -> bool
|
||||
|
||||
global alive
|
||||
|
||||
if alive:
|
||||
logging.debug('watchdog_task: update_task is alive')
|
||||
alive = False
|
||||
return True
|
||||
else:
|
||||
logging.info('watchdog_task: killing main loop because update_task is no longer alive')
|
||||
main_loop.quit()
|
||||
return False
|
||||
|
||||
return watchdog_task
|
||||
|
||||
|
||||
def main(argv):
|
||||
# type: (List[str]) -> ()
|
||||
|
||||
logging.basicConfig(level=cfg.LOG_LEVEL)
|
||||
logging.info('starting ' + __file__)
|
||||
|
||||
tty = parse_cmdline_args(argv)
|
||||
modbus = init_modbus(tty)
|
||||
|
||||
batteries = identify_batteries(modbus)
|
||||
|
||||
if len(batteries) <= 0:
|
||||
sys.exit(2)
|
||||
|
||||
service = DBusService(service_name=cfg.SERVICE_NAME_PREFIX + tty)
|
||||
|
||||
service.own_properties.set('/ResetBatteries', value=False, writable=True) # initial value = False
|
||||
|
||||
main_loop = gobject.MainLoop()
|
||||
|
||||
service_signals = signals.init_service_signals(batteries)
|
||||
publish_service_signals(service, service_signals)
|
||||
|
||||
update_task = create_update_task(modbus, service, batteries)
|
||||
update_task() # run it right away, so that all props are initialized before anyone can ask
|
||||
watchdog_task = create_watchdog_task(main_loop)
|
||||
|
||||
gobject.timeout_add(cfg.UPDATE_INTERVAL * 2, watchdog_task, priority = gobject.PRIORITY_LOW) # add watchdog first
|
||||
gobject.timeout_add(cfg.UPDATE_INTERVAL, update_task, priority = gobject.PRIORITY_LOW) # call update once every update_interval
|
||||
|
||||
logging.info('starting gobject.MainLoop')
|
||||
main_loop.run()
|
||||
logging.info('gobject.MainLoop was shut down')
|
||||
|
||||
sys.exit(0xFF) # reaches this only on error
|
||||
|
||||
|
||||
main(sys.argv[1:])
|
Binary file not shown.
|
@ -0,0 +1,202 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from traceback import print_exc
|
||||
from os import _exit as os_exit
|
||||
from os import statvfs
|
||||
import logging
|
||||
from functools import update_wrapper
|
||||
import dbus
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)
|
||||
|
||||
# Use this function to make sure the code quits on an unexpected exception. Make sure to use it
|
||||
# when using gobject.idle_add and also gobject.timeout_add.
|
||||
# Without this, the code will just keep running, since gobject does not stop the mainloop on an
|
||||
# exception.
|
||||
# Example: gobject.idle_add(exit_on_error, myfunc, arg1, arg2)
|
||||
def exit_on_error(func, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except:
|
||||
try:
|
||||
print 'exit_on_error: there was an exception. Printing stacktrace will be tryed and then exit'
|
||||
print_exc()
|
||||
except:
|
||||
pass
|
||||
|
||||
# sys.exit() is not used, since that throws an exception, which does not lead to a program
|
||||
# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.
|
||||
os_exit(1)
|
||||
|
||||
|
||||
__vrm_portal_id = None
|
||||
def get_vrm_portal_id():
|
||||
# For the CCGX, the definition of the VRM Portal ID is that it is the mac address of the onboard-
|
||||
# ethernet port (eth0), stripped from its colons (:) and lower case.
|
||||
|
||||
# nice coincidence is that this also works fine when running on your (linux) development computer.
|
||||
|
||||
global __vrm_portal_id
|
||||
|
||||
if __vrm_portal_id:
|
||||
return __vrm_portal_id
|
||||
|
||||
# Assume we are on linux
|
||||
import fcntl, socket, struct
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', 'eth0'[:15]))
|
||||
__vrm_portal_id = ''.join(['%02x' % ord(char) for char in info[18:24]])
|
||||
|
||||
return __vrm_portal_id
|
||||
|
||||
|
||||
# See VE.Can registers - public.docx for definition of this conversion
|
||||
def convert_vreg_version_to_readable(version):
|
||||
def str_to_arr(x, length):
|
||||
a = []
|
||||
for i in range(0, len(x), length):
|
||||
a.append(x[i:i+length])
|
||||
return a
|
||||
|
||||
x = "%x" % version
|
||||
x = x.upper()
|
||||
|
||||
if len(x) == 5 or len(x) == 3 or len(x) == 1:
|
||||
x = '0' + x
|
||||
|
||||
a = str_to_arr(x, 2);
|
||||
|
||||
# remove the first 00 if there are three bytes and it is 00
|
||||
if len(a) == 3 and a[0] == '00':
|
||||
a.remove(0);
|
||||
|
||||
# if we have two or three bytes now, and the first character is a 0, remove it
|
||||
if len(a) >= 2 and a[0][0:1] == '0':
|
||||
a[0] = a[0][1];
|
||||
|
||||
result = ''
|
||||
for item in a:
|
||||
result += ('.' if result != '' else '') + item
|
||||
|
||||
|
||||
result = 'v' + result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_free_space(path):
|
||||
result = -1
|
||||
|
||||
try:
|
||||
s = statvfs(path)
|
||||
result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users
|
||||
except Exception, ex:
|
||||
logger.info("Error while retrieving free space for path %s: %s" % (path, ex))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_load_averages():
|
||||
c = read_file('/proc/loadavg')
|
||||
return c.split(' ')[:3]
|
||||
|
||||
|
||||
# Returns False if it cannot find a machine name. Otherwise returns the string
|
||||
# containing the name
|
||||
def get_machine_name():
|
||||
c = read_file('/proc/device-tree/model')
|
||||
|
||||
if c != False:
|
||||
return c.strip('\x00')
|
||||
|
||||
return read_file('/etc/venus/machine')
|
||||
|
||||
|
||||
# Returns False if it cannot open the file. Otherwise returns its rstripped contents
|
||||
def read_file(path):
|
||||
content = False
|
||||
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
content = f.read().rstrip()
|
||||
except Exception, ex:
|
||||
logger.debug("Error while reading %s: %s" % (path, ex))
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def wrap_dbus_value(value):
|
||||
if value is None:
|
||||
return VEDBUS_INVALID
|
||||
if isinstance(value, float):
|
||||
return dbus.Double(value, variant_level=1)
|
||||
if isinstance(value, bool):
|
||||
return dbus.Boolean(value, variant_level=1)
|
||||
if isinstance(value, int):
|
||||
return dbus.Int32(value, variant_level=1)
|
||||
if isinstance(value, str):
|
||||
return dbus.String(value, variant_level=1)
|
||||
if isinstance(value, unicode):
|
||||
return dbus.String(value, variant_level=1)
|
||||
if isinstance(value, list):
|
||||
if len(value) == 0:
|
||||
# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.
|
||||
# A (signed) integer is dangerous, because an empty list of signed integers is used to encode
|
||||
# an invalid value.
|
||||
return dbus.Array([], signature=dbus.Signature('u'), variant_level=1)
|
||||
return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)
|
||||
if isinstance(value, long):
|
||||
return dbus.Int64(value, variant_level=1)
|
||||
if isinstance(value, dict):
|
||||
# Wrapping the keys of the dictionary causes D-Bus errors like:
|
||||
# 'arguments to dbus_message_iter_open_container() were incorrect,
|
||||
# assertion "(type == DBUS_TYPE_ARRAY && contained_signature &&
|
||||
# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||
|
||||
# _dbus_check_is_valid_signature (contained_signature))" failed in file ...'
|
||||
return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)
|
||||
return value
|
||||
|
||||
|
||||
dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)
|
||||
|
||||
|
||||
def unwrap_dbus_value(val):
|
||||
"""Converts D-Bus values back to the original type. For example if val is of type DBus.Double,
|
||||
a float will be returned."""
|
||||
if isinstance(val, dbus_int_types):
|
||||
return int(val)
|
||||
if isinstance(val, dbus.Double):
|
||||
return float(val)
|
||||
if isinstance(val, dbus.Array):
|
||||
v = [unwrap_dbus_value(x) for x in val]
|
||||
return None if len(v) == 0 else v
|
||||
if isinstance(val, (dbus.Signature, dbus.String)):
|
||||
return unicode(val)
|
||||
# Python has no byte type, so we convert to an integer.
|
||||
if isinstance(val, dbus.Byte):
|
||||
return int(val)
|
||||
if isinstance(val, dbus.ByteArray):
|
||||
return "".join([str(x) for x in val])
|
||||
if isinstance(val, (list, tuple)):
|
||||
return [unwrap_dbus_value(x) for x in val]
|
||||
if isinstance(val, (dbus.Dictionary, dict)):
|
||||
# Do not unwrap the keys, see comment in wrap_dbus_value
|
||||
return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])
|
||||
if isinstance(val, dbus.Boolean):
|
||||
return bool(val)
|
||||
return val
|
||||
|
||||
class reify(object):
|
||||
""" Decorator to replace a property of an object with the calculated value,
|
||||
to make it concrete. """
|
||||
def __init__(self, wrapped):
|
||||
self.wrapped = wrapped
|
||||
update_wrapper(self, wrapped)
|
||||
def __get__(self, inst, objtype=None):
|
||||
if inst is None:
|
||||
return self
|
||||
v = self.wrapped(inst)
|
||||
setattr(inst, self.wrapped.__name__, v)
|
||||
return v
|
Binary file not shown.
|
@ -0,0 +1,496 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import dbus.service
|
||||
import logging
|
||||
import traceback
|
||||
import os
|
||||
import weakref
|
||||
from ve_utils import wrap_dbus_value, unwrap_dbus_value
|
||||
|
||||
# vedbus contains three classes:
|
||||
# VeDbusItemImport -> use this to read data from the dbus, ie import
|
||||
# VeDbusItemExport -> use this to export data to the dbus (one value)
|
||||
# VeDbusService -> use that to create a service and export several values to the dbus
|
||||
|
||||
# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.
|
||||
# All projects that used busitem.py need to migrate to this package. And some
|
||||
# projects used to define there own equivalent of VeDbusItemExport. Better to
|
||||
# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.
|
||||
|
||||
# TODOS
|
||||
# 1 check for datatypes, it works now, but not sure if all is compliant with
|
||||
# com.victronenergy.BusItem interface definition. See also the files in
|
||||
# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps
|
||||
# something similar should also be done in VeDbusBusItemExport?
|
||||
# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?
|
||||
# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking
|
||||
# changes possible. Does everybody first invalidate its data before leaving the bus?
|
||||
# And what about before taking one object away from the bus, instead of taking the
|
||||
# whole service offline?
|
||||
# They should! And after taking one value away, do we need to know that someone left
|
||||
# the bus? Or we just keep that value in invalidated for ever? Result is that we can't
|
||||
# see the difference anymore between an invalidated value and a value that was first on
|
||||
# the bus and later not anymore. See comments above VeDbusItemImport as well.
|
||||
# 9 there are probably more todos in the code below.
|
||||
|
||||
# Some thoughts with regards to the data types:
|
||||
#
|
||||
# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types
|
||||
# ---
|
||||
# Variants are represented by setting the variant_level keyword argument in the
|
||||
# constructor of any D-Bus data type to a value greater than 0 (variant_level 1
|
||||
# means a variant containing some other data type, variant_level 2 means a variant
|
||||
# containing a variant containing some other data type, and so on). If a non-variant
|
||||
# is passed as an argument but introspection indicates that a variant is expected,
|
||||
# it'll automatically be wrapped in a variant.
|
||||
# ---
|
||||
#
|
||||
# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass
|
||||
# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera
|
||||
#
|
||||
# So all together that explains why we don't need to explicitly convert back and forth
|
||||
# between the dbus datatypes and the standard python datatypes. Note that all datatypes
|
||||
# in python are objects. Even an int is an object.
|
||||
|
||||
# The signature of a variant is 'v'.
|
||||
|
||||
# Export ourselves as a D-Bus service.
|
||||
class VeDbusService(object):
|
||||
def __init__(self, servicename, bus=None):
|
||||
# dict containing the VeDbusItemExport objects, with their path as the key.
|
||||
self._dbusobjects = {}
|
||||
self._dbusnodes = {}
|
||||
|
||||
# dict containing the onchange callbacks, for each object. Object path is the key
|
||||
self._onchangecallbacks = {}
|
||||
|
||||
# Connect to session bus whenever present, else use the system bus
|
||||
self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())
|
||||
|
||||
# make the dbus connection available to outside, could make this a true property instead, but ach..
|
||||
self.dbusconn = self._dbusconn
|
||||
|
||||
# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)
|
||||
self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True)
|
||||
|
||||
# Add the root item that will return all items as a tree
|
||||
self._dbusnodes['/'] = self._create_tree_export(self._dbusconn, '/', self._get_tree_dict)
|
||||
|
||||
logging.info("registered ourselves on D-Bus as %s" % servicename)
|
||||
|
||||
def _get_tree_dict(self, path, get_text=False):
|
||||
logging.debug("_get_tree_dict called for %s" % path)
|
||||
r = {}
|
||||
px = path
|
||||
if not px.endswith('/'):
|
||||
px += '/'
|
||||
for p, item in self._dbusobjects.items():
|
||||
if p.startswith(px):
|
||||
v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())
|
||||
r[p[len(px):]] = v
|
||||
logging.debug(r)
|
||||
return r
|
||||
|
||||
# To force immediate deregistering of this dbus service and all its object paths, explicitly
|
||||
# call __del__().
|
||||
def __del__(self):
|
||||
for node in self._dbusnodes.values():
|
||||
node.__del__()
|
||||
self._dbusnodes.clear()
|
||||
for item in self._dbusobjects.values():
|
||||
item.__del__()
|
||||
self._dbusobjects.clear()
|
||||
if self._dbusname:
|
||||
self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code
|
||||
self._dbusname = None
|
||||
|
||||
# @param callbackonchange function that will be called when this value is changed. First parameter will
|
||||
# be the path of the object, second the new value. This callback should return
|
||||
# True to accept the change, False to reject it.
|
||||
def add_path(self, path, value, description="", writeable=False,
|
||||
onchangecallback=None, gettextcallback=None):
|
||||
|
||||
if onchangecallback is not None:
|
||||
self._onchangecallbacks[path] = onchangecallback
|
||||
|
||||
item = VeDbusItemExport(
|
||||
self._dbusconn, path, value, description, writeable,
|
||||
self._value_changed, gettextcallback, deletecallback=self._item_deleted)
|
||||
|
||||
spl = path.split('/')
|
||||
for i in range(2, len(spl)):
|
||||
subPath = '/'.join(spl[:i])
|
||||
if subPath not in self._dbusnodes and subPath not in self._dbusobjects:
|
||||
self._dbusnodes[subPath] = self._create_tree_export(self._dbusconn, subPath, self._get_tree_dict)
|
||||
self._dbusobjects[path] = item
|
||||
logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))
|
||||
|
||||
# Add the mandatory paths, as per victron dbus api doc
|
||||
def add_mandatory_paths(self, processname, processversion, connection,
|
||||
deviceinstance, productid, productname, firmwareversion, hardwareversion, connected):
|
||||
self.add_path('/Mgmt/ProcessName', processname)
|
||||
self.add_path('/Mgmt/ProcessVersion', processversion)
|
||||
self.add_path('/Mgmt/Connection', connection)
|
||||
|
||||
# Create rest of the mandatory objects
|
||||
self.add_path('/DeviceInstance', deviceinstance)
|
||||
self.add_path('/ProductId', productid)
|
||||
self.add_path('/ProductName', productname)
|
||||
self.add_path('/FirmwareVersion', firmwareversion)
|
||||
self.add_path('/HardwareVersion', hardwareversion)
|
||||
self.add_path('/Connected', connected)
|
||||
|
||||
def _create_tree_export(self, bus, objectPath, get_value_handler):
|
||||
return VeDbusTreeExport(bus, objectPath, get_value_handler)
|
||||
|
||||
# Callback function that is called from the VeDbusItemExport objects when a value changes. This function
|
||||
# maps the change-request to the onchangecallback given to us for this specific path.
|
||||
def _value_changed(self, path, newvalue):
|
||||
if path not in self._onchangecallbacks:
|
||||
return True
|
||||
|
||||
return self._onchangecallbacks[path](path, newvalue)
|
||||
|
||||
def _item_deleted(self, path):
|
||||
self._dbusobjects.pop(path)
|
||||
for np in self._dbusnodes.keys():
|
||||
if np != '/':
|
||||
for ip in self._dbusobjects:
|
||||
if ip.startswith(np + '/'):
|
||||
break
|
||||
else:
|
||||
self._dbusnodes[np].__del__()
|
||||
self._dbusnodes.pop(np)
|
||||
|
||||
def __getitem__(self, path):
|
||||
return self._dbusobjects[path].local_get_value()
|
||||
|
||||
def __setitem__(self, path, newvalue):
|
||||
self._dbusobjects[path].local_set_value(newvalue)
|
||||
|
||||
def __delitem__(self, path):
|
||||
self._dbusobjects[path].__del__() # Invalidates and then removes the object path
|
||||
assert path not in self._dbusobjects
|
||||
|
||||
def __contains__(self, path):
|
||||
return path in self._dbusobjects
|
||||
|
||||
"""
|
||||
Importing basics:
|
||||
- If when we power up, the D-Bus service does not exist, or it does exist and the path does not
|
||||
yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its
|
||||
initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,
|
||||
call the eventCallback.
|
||||
- If when we power up, save it
|
||||
- When using get_value, know that there is no difference between services (or object paths) that don't
|
||||
exist and paths that are invalid (= empty array, see above). Both will return None. In case you do
|
||||
really want to know ifa path exists or not, use the exists property.
|
||||
- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals
|
||||
with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-
|
||||
signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this
|
||||
class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this
|
||||
class.
|
||||
|
||||
Read when using this class:
|
||||
Note that when a service leaves that D-Bus without invalidating all its exported objects first, for
|
||||
example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,
|
||||
make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,
|
||||
because that takes care of all of that for you.
|
||||
"""
|
||||
class VeDbusItemImport(object):
|
||||
## Constructor
|
||||
# @param bus the bus-object (SESSION or SYSTEM).
|
||||
# @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'
|
||||
# @param path the object-path, for example '/Dc/V'
|
||||
# @param eventCallback function that you want to be called on a value change
|
||||
# @param createSignal only set this to False if you use this function to one time read a value. When
|
||||
# leaving it to True, make sure to also subscribe to the NameOwnerChanged signal
|
||||
# elsewhere. See also note some 15 lines up.
|
||||
def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):
|
||||
# TODO: is it necessary to store _serviceName and _path? Isn't it
|
||||
# stored in the bus_getobjectsomewhere?
|
||||
self._serviceName = serviceName
|
||||
self._path = path
|
||||
self._match = None
|
||||
# TODO: _proxy is being used in settingsdevice.py, make a getter for that
|
||||
self._proxy = bus.get_object(serviceName, path, introspect=False)
|
||||
self.eventCallback = eventCallback
|
||||
|
||||
assert eventCallback is None or createsignal == True
|
||||
if createsignal:
|
||||
self._match = self._proxy.connect_to_signal(
|
||||
"PropertiesChanged", weak_functor(self._properties_changed_handler))
|
||||
|
||||
# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to
|
||||
# None, same as when a value is invalid
|
||||
self._cachedvalue = None
|
||||
try:
|
||||
v = self._proxy.GetValue()
|
||||
except dbus.exceptions.DBusException:
|
||||
pass
|
||||
else:
|
||||
self._cachedvalue = unwrap_dbus_value(v)
|
||||
|
||||
def __del__(self):
|
||||
if self._match != None:
|
||||
self._match.remove()
|
||||
self._match = None
|
||||
self._proxy = None
|
||||
|
||||
def _refreshcachedvalue(self):
|
||||
self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())
|
||||
|
||||
## Returns the path as a string, for example '/AC/L1/V'
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1
|
||||
@property
|
||||
def serviceName(self):
|
||||
return self._serviceName
|
||||
|
||||
## Returns the value of the dbus-item.
|
||||
# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)
|
||||
# this is not a property to keep the name consistant with the com.victronenergy.busitem interface
|
||||
# returns None when the property is invalid
|
||||
def get_value(self):
|
||||
return self._cachedvalue
|
||||
|
||||
## Writes a new value to the dbus-item
|
||||
def set_value(self, newvalue):
|
||||
r = self._proxy.SetValue(wrap_dbus_value(newvalue))
|
||||
|
||||
# instead of just saving the value, go to the dbus and get it. So we have the right type etc.
|
||||
if r == 0:
|
||||
self._refreshcachedvalue()
|
||||
|
||||
return r
|
||||
|
||||
## Returns the text representation of the value.
|
||||
# For example when the value is an enum/int GetText might return the string
|
||||
# belonging to that enum value. Another example, for a voltage, GetValue
|
||||
# would return a float, 12.0Volt, and GetText could return 12 VDC.
|
||||
#
|
||||
# Note that this depends on how the dbus-producer has implemented this.
|
||||
def get_text(self):
|
||||
return self._proxy.GetText()
|
||||
|
||||
## Returns true of object path exists, and false if it doesn't
|
||||
@property
|
||||
def exists(self):
|
||||
# TODO: do some real check instead of this crazy thing.
|
||||
r = False
|
||||
try:
|
||||
r = self._proxy.GetValue()
|
||||
r = True
|
||||
except dbus.exceptions.DBusException:
|
||||
pass
|
||||
|
||||
return r
|
||||
|
||||
## callback for the trigger-event.
|
||||
# @param eventCallback the event-callback-function.
|
||||
@property
|
||||
def eventCallback(self):
|
||||
return self._eventCallback
|
||||
|
||||
@eventCallback.setter
|
||||
def eventCallback(self, eventCallback):
|
||||
self._eventCallback = eventCallback
|
||||
|
||||
## Is called when the value of the imported bus-item changes.
|
||||
# Stores the new value in our local cache, and calls the eventCallback, if set.
|
||||
def _properties_changed_handler(self, changes):
|
||||
if "Value" in changes:
|
||||
changes['Value'] = unwrap_dbus_value(changes['Value'])
|
||||
self._cachedvalue = changes['Value']
|
||||
if self._eventCallback:
|
||||
# The reason behind this try/except is to prevent errors silently ending up the an error
|
||||
# handler in the dbus code.
|
||||
try:
|
||||
self._eventCallback(self._serviceName, self._path, changes)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
os._exit(1) # sys.exit() is not used, since that also throws an exception
|
||||
|
||||
|
||||
class VeDbusTreeExport(dbus.service.Object):
|
||||
def __init__(self, bus, objectPath, get_value_handler):
|
||||
dbus.service.Object.__init__(self, bus, objectPath)
|
||||
self._get_value_handler = get_value_handler
|
||||
logging.debug("VeDbusTreeExport %s has been created" % objectPath)
|
||||
|
||||
def __del__(self):
|
||||
# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,
|
||||
# so we need a copy.
|
||||
path = self._get_path()
|
||||
if path is None:
|
||||
return
|
||||
self.remove_from_connection()
|
||||
logging.debug("VeDbusTreeExport %s has been removed" % path)
|
||||
|
||||
def _get_path(self):
|
||||
if len(self._locations) == 0:
|
||||
return None
|
||||
return self._locations[0][1]
|
||||
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
|
||||
def GetValue(self):
|
||||
value = self._get_value_handler(self._get_path())
|
||||
return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)
|
||||
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
|
||||
def GetText(self):
|
||||
return self._get_value_handler(self._get_path(), True)
|
||||
|
||||
def local_get_value(self):
|
||||
return self._get_value_handler(self.path)
|
||||
|
||||
|
||||
class VeDbusItemExport(dbus.service.Object):
|
||||
## Constructor of VeDbusItemExport
|
||||
#
|
||||
# Use this object to export (publish), values on the dbus
|
||||
# Creates the dbus-object under the given dbus-service-name.
|
||||
# @param bus The dbus object.
|
||||
# @param objectPath The dbus-object-path.
|
||||
# @param value Value to initialize ourselves with, defaults to None which means Invalid
|
||||
# @param description String containing a description. Can be called over the dbus with GetDescription()
|
||||
# @param writeable what would this do!? :).
|
||||
# @param callback Function that will be called when someone else changes the value of this VeBusItem
|
||||
# over the dbus. First parameter passed to callback will be our path, second the new
|
||||
# value. This callback should return True to accept the change, False to reject it.
|
||||
def __init__(self, bus, objectPath, value=None, description=None, writeable=False,
|
||||
onchangecallback=None, gettextcallback=None, deletecallback=None):
|
||||
dbus.service.Object.__init__(self, bus, objectPath)
|
||||
self._onchangecallback = onchangecallback
|
||||
self._gettextcallback = gettextcallback
|
||||
self._value = value
|
||||
self._description = description
|
||||
self._writeable = writeable
|
||||
self._deletecallback = deletecallback
|
||||
|
||||
# To force immediate deregistering of this dbus object, explicitly call __del__().
|
||||
def __del__(self):
|
||||
# self._get_path() will raise an exception when retrieved after the
|
||||
# call to .remove_from_connection, so we need a copy.
|
||||
path = self._get_path()
|
||||
if path == None:
|
||||
return
|
||||
if self._deletecallback is not None:
|
||||
self._deletecallback(path)
|
||||
self.local_set_value(None)
|
||||
self.remove_from_connection()
|
||||
logging.debug("VeDbusItemExport %s has been removed" % path)
|
||||
|
||||
def _get_path(self):
|
||||
if len(self._locations) == 0:
|
||||
return None
|
||||
return self._locations[0][1]
|
||||
|
||||
## Sets the value. And in case the value is different from what it was, a signal
|
||||
# will be emitted to the dbus. This function is to be used in the python code that
|
||||
# is using this class to export values to the dbus.
|
||||
# set value to None to indicate that it is Invalid
|
||||
def local_set_value(self, newvalue):
|
||||
if self._value == newvalue:
|
||||
return
|
||||
|
||||
self._value = newvalue
|
||||
|
||||
changes = {}
|
||||
changes['Value'] = wrap_dbus_value(newvalue)
|
||||
changes['Text'] = self.GetText()
|
||||
self.PropertiesChanged(changes)
|
||||
|
||||
def local_get_value(self):
|
||||
return self._value
|
||||
|
||||
# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====
|
||||
|
||||
## Dbus exported method SetValue
|
||||
# Function is called over the D-Bus by other process. It will first check (via callback) if new
|
||||
# value is accepted. And it is, stores it and emits a changed-signal.
|
||||
# @param value The new value.
|
||||
# @return completion-code When successful a 0 is return, and when not a -1 is returned.
|
||||
@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')
|
||||
def SetValue(self, newvalue):
|
||||
if not self._writeable:
|
||||
return 1 # NOT OK
|
||||
|
||||
newvalue = unwrap_dbus_value(newvalue)
|
||||
|
||||
if newvalue == self._value:
|
||||
return 0 # OK
|
||||
|
||||
# call the callback given to us, and check if new value is OK.
|
||||
if (self._onchangecallback is None or
|
||||
(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):
|
||||
|
||||
self.local_set_value(newvalue)
|
||||
return 0 # OK
|
||||
|
||||
return 2 # NOT OK
|
||||
|
||||
## Dbus exported method GetDescription
|
||||
#
|
||||
# Returns the a description.
|
||||
# @param language A language code (e.g. ISO 639-1 en-US).
|
||||
# @param length Lenght of the language string.
|
||||
# @return description
|
||||
@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')
|
||||
def GetDescription(self, language, length):
|
||||
return self._description if self._description is not None else 'No description given'
|
||||
|
||||
## Dbus exported method GetValue
|
||||
# Returns the value.
|
||||
# @return the value when valid, and otherwise an empty array
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
|
||||
def GetValue(self):
|
||||
return wrap_dbus_value(self._value)
|
||||
|
||||
## Dbus exported method GetText
|
||||
# Returns the value as string of the dbus-object-path.
|
||||
# @return text A text-value. '---' when local value is invalid
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='s')
|
||||
def GetText(self):
|
||||
if self._value is None:
|
||||
return '---'
|
||||
|
||||
# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we
|
||||
# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from
|
||||
# the application itself, as all data from the D-Bus should have been unwrapped by now.
|
||||
if self._gettextcallback is None and type(self._value) == dbus.Byte:
|
||||
return str(int(self._value))
|
||||
|
||||
if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':
|
||||
return "0x%X" % self._value
|
||||
|
||||
if self._gettextcallback is None:
|
||||
return str(self._value)
|
||||
|
||||
return self._gettextcallback(self.__dbus_object_path__, self._value)
|
||||
|
||||
## The signal that indicates that the value has changed.
|
||||
# Other processes connected to this BusItem object will have subscribed to the
|
||||
# event when they want to track our state.
|
||||
@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')
|
||||
def PropertiesChanged(self, changes):
|
||||
pass
|
||||
|
||||
## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference
|
||||
## to the object which method is to be called.
|
||||
## Use this object to break circular references.
|
||||
class weak_functor:
|
||||
def __init__(self, f):
|
||||
self._r = weakref.ref(f.__self__)
|
||||
self._f = weakref.ref(f.__func__)
|
||||
|
||||
def __call__(self, *args, **kargs):
|
||||
r = self._r()
|
||||
f = self._f()
|
||||
if r == None or f == None:
|
||||
return
|
||||
f(r, *args, **kargs)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,54 @@
|
|||
from logging import getLogger
|
||||
from python_libs.ie_utils.mixins import Disposable, RequiresMainLoop, Record
|
||||
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
|
||||
from python_libs.ie_dbus.private.own_properties import OwnProperties
|
||||
from python_libs.ie_dbus.private.remote_properties import RemoteProperties
|
||||
from python_libs.ie_dbus.private.ve_constants import SERVICE_PREFIX
|
||||
from python_libs.ie_dbus.private.settings import Settings
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Union, AnyStr, NoReturn, List
|
||||
|
||||
|
||||
def _enforce_ve_prefix(service_name_filter):
|
||||
if not service_name_filter.startswith(SERVICE_PREFIX):
|
||||
raise ValueError('service_name_filter must start with ' + SERVICE_PREFIX)
|
||||
|
||||
|
||||
SESSION_BUS = 0
|
||||
SYSTEM_BUS = 1
|
||||
|
||||
|
||||
class DBusService(Record, Disposable, RequiresMainLoop):
|
||||
|
||||
def __init__(self, service_name=None, device_instance=1, connection_type_or_address=SYSTEM_BUS):
|
||||
# type: (str, int, Union[int, AnyStr]) -> NoReturn
|
||||
|
||||
service_name = service_name if service_name.startswith(SERVICE_PREFIX) else SERVICE_PREFIX + service_name
|
||||
|
||||
self._daemon = DBusDaemon(connection_type_or_address)
|
||||
self.remote_properties = RemoteProperties(self._daemon)
|
||||
self.own_properties = OwnProperties(self._daemon)
|
||||
self.own_properties.set('/DeviceInstance', device_instance) # must be set before request_name, sigh
|
||||
|
||||
self.settings = Settings(self._daemon, self.remote_properties)
|
||||
self.name = service_name
|
||||
|
||||
if service_name is not None:
|
||||
self._bus_name = self._daemon.request_name(service_name)
|
||||
_log.info('service name is ' + service_name)
|
||||
|
||||
_log.info('id is ' + self.bus_id)
|
||||
|
||||
@property
|
||||
def available_services(self):
|
||||
# type: () -> List[unicode]
|
||||
return [s.name for s in self._daemon.services]
|
||||
|
||||
@property
|
||||
def bus_id(self):
|
||||
# type: () -> unicode
|
||||
return self._daemon.bus_id
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,22 @@
|
|||
from logging import getLogger
|
||||
|
||||
from python_libs.ie_utils.mixins import Record
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import AnyStr
|
||||
|
||||
|
||||
class ServiceInfo(Record):
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def __init__(self, name, id, pid, proc_name, cmd):
|
||||
# type: (AnyStr, AnyStr, int, str, str) -> ServiceInfo
|
||||
|
||||
self.proc_name = proc_name
|
||||
self.name = name
|
||||
self.id = id
|
||||
self.cmd = cmd
|
||||
self.pid = pid
|
Binary file not shown.
|
@ -0,0 +1,185 @@
|
|||
from logging import getLogger
|
||||
|
||||
from _dbus_bindings import Connection, MethodCallMessage, SignalMessage, BUS_DAEMON_NAME, \
|
||||
BUS_DAEMON_PATH, BUS_DAEMON_IFACE, NAME_FLAG_DO_NOT_QUEUE, Message, HANDLER_RESULT_HANDLED
|
||||
|
||||
from python_libs.ie_dbus.private.dbus_types import dbus_string, dbus_uint32
|
||||
from python_libs.ie_dbus.private.message_types import DBusException
|
||||
from python_libs.ie_utils.mixins import Disposable
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import List, Optional, Iterable, Callable, Union, NoReturn, AnyStr, Any
|
||||
from python_libs.ie_dbus.private.dbus_types import DbusType
|
||||
|
||||
|
||||
class DbusConnection(Disposable):
|
||||
"""
|
||||
A collection of stateless functions operating on a Connection object
|
||||
"""
|
||||
|
||||
def __init__(self, connection_type_or_address):
|
||||
# type: (Union[int, AnyStr]) -> NoReturn
|
||||
|
||||
self._address = connection_type_or_address
|
||||
# noinspection PyProtectedMember
|
||||
self._connection = Connection._new_for_bus(connection_type_or_address) # it's not disposable
|
||||
self.chain_disposable(self._connection.close, 'connection ' + self._connection.get_unique_name())
|
||||
|
||||
@property
|
||||
def bus_id(self):
|
||||
return self._connection.get_unique_name()
|
||||
|
||||
def fork(self):
|
||||
return DbusConnection(self._address)
|
||||
|
||||
def get_ids_and_service_names(self):
|
||||
# type: () -> Iterable[unicode]
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return map(unicode, self.call_daemon_method('ListNames')[0])
|
||||
|
||||
def get_service_names(self):
|
||||
# type: () -> Iterable[AnyStr]
|
||||
|
||||
return (
|
||||
unicode(name)
|
||||
for name
|
||||
in self.get_ids_and_service_names()
|
||||
if not name.startswith(':')
|
||||
)
|
||||
|
||||
def get_service_ids(self):
|
||||
# type: () -> Iterable[AnyStr]
|
||||
|
||||
return (
|
||||
name
|
||||
for name in self.get_ids_and_service_names() if name.startswith(':'))
|
||||
|
||||
# noinspection PyBroadException
|
||||
def get_pid_of_service(self, service_name):
|
||||
# type: (AnyStr) -> Optional[int]
|
||||
try:
|
||||
reply = self.call_daemon_method('GetConnectionUnixProcessID', dbus_string(service_name))
|
||||
return int(reply[0])
|
||||
except:
|
||||
return None
|
||||
|
||||
def get_id_of_service(self, service_name):
|
||||
# type: (AnyStr) -> AnyStr
|
||||
reply = self.call_daemon_method('GetNameOwner', dbus_string(service_name))
|
||||
return unicode(reply[0])
|
||||
|
||||
def call_method(self, service_name, object_path, interface, member, *args):
|
||||
# type: (AnyStr, AnyStr, Optional[str], str, List[Any]) -> List[Any]
|
||||
|
||||
msg = MethodCallMessage(service_name, object_path, interface, member)
|
||||
|
||||
for arg in args:
|
||||
msg.append(arg)
|
||||
|
||||
reply = self._connection.send_message_with_reply_and_block(msg) # with py3 we could use asyncio here
|
||||
DBusException.raise_if_error_reply(reply)
|
||||
|
||||
return reply.get_args_list() # TODO: utf8_strings=True ?
|
||||
|
||||
def send_message(self, msg):
|
||||
# type: (Message) -> NoReturn
|
||||
|
||||
self._connection.send_message(msg)
|
||||
|
||||
def call_daemon_method(self, method_name, *args):
|
||||
# type: (AnyStr, Iterable[DbusType])-> List[any]
|
||||
|
||||
return self.call_method(BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE, method_name, *args)
|
||||
|
||||
def request_name(self, service_name):
|
||||
# type: (AnyStr) -> Disposable
|
||||
|
||||
_log.debug('requesting bus name ' + service_name)
|
||||
|
||||
self.call_daemon_method('RequestName', dbus_string(service_name), dbus_uint32(NAME_FLAG_DO_NOT_QUEUE))
|
||||
|
||||
def dispose():
|
||||
self.call_daemon_method('ReleaseName', dbus_string(service_name))
|
||||
|
||||
return self.create_dependent_disposable(dispose, 'bus name ' + service_name)
|
||||
|
||||
def broadcast_signal(self, object_path, interface, member, *args):
|
||||
# type: (AnyStr, AnyStr, AnyStr, List[Any]) -> NoReturn
|
||||
|
||||
msg = SignalMessage(object_path, interface, member)
|
||||
for arg in args:
|
||||
msg.append(arg)
|
||||
|
||||
self._connection.send_message(msg)
|
||||
|
||||
def add_message_callback(self, callback, filter_rule, fork=True):
|
||||
# type: (Callable[[Message], NoReturn], AnyStr, Optional[bool]) -> Disposable
|
||||
if fork:
|
||||
return self._add_message_callback_fork(callback, filter_rule)
|
||||
else:
|
||||
return self._add_message_callback_no_fork(callback, filter_rule)
|
||||
|
||||
def _add_message_callback_no_fork(self, callback, filter_rule): # TODO: forking for incoming method calls
|
||||
# type: (Callable[[Message], NoReturn], AnyStr) -> Disposable
|
||||
|
||||
def dispatch(_, msg):
|
||||
# type: (Connection, Message) -> int
|
||||
|
||||
#_log.info(' ####### got message type=' + str(msg.get_type()) + ' ' + msg.get_path() + '/' + msg.get_member())
|
||||
callback(msg)
|
||||
#_log.debug('DONE')
|
||||
return HANDLER_RESULT_HANDLED
|
||||
|
||||
msg_filter = self._add_message_filter(dispatch)
|
||||
match = self._add_match(filter_rule)
|
||||
|
||||
def dispose():
|
||||
match.dispose()
|
||||
msg_filter.dispose()
|
||||
|
||||
return self.create_dependent_disposable(dispose)
|
||||
|
||||
def _add_message_callback_fork(self, callback, filter_rule):
|
||||
# type: (Callable[[Message], NoReturn], AnyStr) -> Disposable
|
||||
|
||||
forked = self.fork()
|
||||
_log.debug('forked connection ' + forked.bus_id)
|
||||
|
||||
def dispatch(_, msg):
|
||||
# type: (Connection, Message) -> int
|
||||
|
||||
# _log.debug('got message type=' + str(msg.get_type()) + ' ' + msg.get_path() + '/' + msg.get_member())
|
||||
callback(msg)
|
||||
return HANDLER_RESULT_HANDLED
|
||||
|
||||
forked._add_message_filter(dispatch)
|
||||
forked._add_match(filter_rule)
|
||||
|
||||
return self.create_dependent_disposable(forked)
|
||||
|
||||
def _add_message_filter(self, callback):
|
||||
# type: (Callable[[Connection, Message], int]) -> Disposable
|
||||
|
||||
_log.debug('added filter on ' + self.bus_id)
|
||||
self._connection.add_message_filter(callback)
|
||||
|
||||
def dispose():
|
||||
self._connection.remove_message_filter(callback)
|
||||
|
||||
return self.create_dependent_disposable(dispose, 'message filter on ' + self.bus_id)
|
||||
|
||||
def _add_match(self, filter_rule):
|
||||
# type: (AnyStr) -> Disposable
|
||||
|
||||
self.call_daemon_method('AddMatch', dbus_string(filter_rule))
|
||||
|
||||
_log.debug('added match_rule: ' + filter_rule)
|
||||
|
||||
def dispose():
|
||||
self.call_daemon_method('RemoveMatch', dbus_string(filter_rule))
|
||||
|
||||
return self.create_dependent_disposable(dispose, 'Match ' + filter_rule)
|
Binary file not shown.
|
@ -0,0 +1,273 @@
|
|||
from logging import getLogger
|
||||
|
||||
from _dbus_bindings import Message, ErrorMessage, BUS_DAEMON_NAME, BUS_DAEMON_PATH, BUS_DAEMON_IFACE
|
||||
from python_libs.ie_dbus.private.datatypes import ServiceInfo
|
||||
from python_libs.ie_dbus.private.dbus_connection import DbusConnection
|
||||
from python_libs.ie_dbus.private.message_types import MatchedMessage, MessageFilter, ResolvedMessage
|
||||
from python_libs.ie_utils.mixins import Disposable, RequiresMainLoop
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
NONE = '<none>'
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, List, Optional, Iterable, Union, AnyStr, NoReturn, Any, Dict
|
||||
from python_libs.ie_dbus.private.dbus_types import DbusType
|
||||
|
||||
|
||||
class DBusDaemon(Disposable, RequiresMainLoop):
|
||||
|
||||
_services = None # type: Dict[str, ServiceInfo]
|
||||
|
||||
def __init__(self, connection_type_or_address):
|
||||
# type: (Union[int, AnyStr]) -> NoReturn
|
||||
|
||||
self._dbus = DbusConnection(connection_type_or_address)
|
||||
# self._dbus.add_message_callback(lambda _: None, 'type=method_call', fork=False) # sink method calls, TODO
|
||||
|
||||
self._name_changed = self.subscribe_to_signal_message(
|
||||
self._on_name_owner_changed,
|
||||
sender_id=BUS_DAEMON_NAME,
|
||||
object_path=BUS_DAEMON_PATH,
|
||||
interface=BUS_DAEMON_IFACE,
|
||||
member='NameOwnerChanged')
|
||||
|
||||
self._services = self._init_services()
|
||||
|
||||
@property
|
||||
def bus_id(self):
|
||||
# type: () -> AnyStr
|
||||
return self._dbus.bus_id
|
||||
|
||||
@property
|
||||
def services(self):
|
||||
# type: () -> Iterable[ServiceInfo]
|
||||
return self._services.itervalues()
|
||||
|
||||
def subscribe_to_signal_message(
|
||||
self,
|
||||
callback,
|
||||
sender_id='*',
|
||||
sender_name='*',
|
||||
object_path='*',
|
||||
interface='*',
|
||||
member='*',
|
||||
signature='*'):
|
||||
# type: (Callable[[MatchedMessage], None], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr]) -> Disposable
|
||||
|
||||
message_filter = MessageFilter(
|
||||
message_type='signal',
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
object_path=object_path,
|
||||
interface=interface,
|
||||
member=member,
|
||||
signature=signature)
|
||||
|
||||
def dispatch(msg):
|
||||
# type: (Message) -> NoReturn
|
||||
|
||||
resolved_msg = self._resolve_message(msg)
|
||||
matched = message_filter.match_message(resolved_msg)
|
||||
|
||||
if matched is not None:
|
||||
callback(matched)
|
||||
|
||||
return self._dbus.add_message_callback(dispatch, message_filter.filter_rule)
|
||||
|
||||
def subscribe_to_method_call_message(
|
||||
self,
|
||||
callback,
|
||||
sender_id='*',
|
||||
sender_name='*',
|
||||
object_path='*',
|
||||
interface='*',
|
||||
member='*',
|
||||
signature='*',
|
||||
destination_id='*',
|
||||
destination_name='*'):
|
||||
# type: (Callable[[MatchedMessage], Any], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[AnyStr], Optional[bool]) -> Disposable
|
||||
|
||||
message_filter = MessageFilter(
|
||||
message_type='method_call',
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
object_path=object_path,
|
||||
interface=interface,
|
||||
member=member,
|
||||
signature=signature,
|
||||
destination_id=destination_id,
|
||||
destination_name=destination_name) # TODO: eavesdrop logic
|
||||
|
||||
def dispatch(msg):
|
||||
# type: (Message) -> NoReturn
|
||||
|
||||
if msg.get_type() != 1:
|
||||
return
|
||||
|
||||
resolved_msg = self._resolve_message(msg)
|
||||
matched = message_filter.match_message(resolved_msg)
|
||||
|
||||
if matched is None:
|
||||
reply = ErrorMessage(msg, 'com.victronenergy.method_call_refused', 'refused')
|
||||
else:
|
||||
try:
|
||||
result = callback(matched)
|
||||
except Exception as e:
|
||||
# _log.debug('method_call threw an exception ' + str(e))
|
||||
# traceback.print_exc()
|
||||
reply = matched.create_error_reply(e)
|
||||
else:
|
||||
reply = matched.create_method_reply(result)
|
||||
|
||||
self._dbus.send_message(reply)
|
||||
|
||||
return self._dbus.add_message_callback(dispatch, message_filter.filter_rule, fork=False)
|
||||
|
||||
def request_name(self, service_name):
|
||||
# type: (AnyStr) -> Disposable
|
||||
|
||||
return self._dbus.request_name(service_name)
|
||||
|
||||
def call_method(self, service_name, object_path, interface, member, *args):
|
||||
# type: (AnyStr, AnyStr, AnyStr, AnyStr, Iterable[DbusType]) -> List[Any]
|
||||
|
||||
return self._dbus.call_method(service_name, object_path, interface, member, *args)
|
||||
|
||||
def broadcast_signal(self, object_path, interface, member, *args):
|
||||
# type: (AnyStr, AnyStr, AnyStr, List[DbusType]) -> NoReturn
|
||||
|
||||
self._dbus.broadcast_signal(object_path, interface, member, *args)
|
||||
|
||||
def get_service_names_of_id(self, service_id):
|
||||
# type: (str) -> List[AnyStr]
|
||||
|
||||
if service_id is None:
|
||||
return []
|
||||
|
||||
return [
|
||||
s.name
|
||||
for s in self.services
|
||||
if s.id == service_id
|
||||
]
|
||||
|
||||
def get_id_for_service_name(self, service_name):
|
||||
# type: (AnyStr) -> Optional[AnyStr]
|
||||
|
||||
return next((s.id for s in self.services if s.name == service_name), None)
|
||||
|
||||
def exists_service_with_name(self, service_name):
|
||||
# type: (AnyStr) -> bool
|
||||
|
||||
return self.get_id_for_service_name(service_name) is not None
|
||||
|
||||
def _resolve_message(self, msg):
|
||||
# type: (Message) -> ResolvedMessage
|
||||
|
||||
sender_id, sender_names = self._resolve_name(msg.get_sender())
|
||||
destination_id, destination_names = self._resolve_name(msg.get_destination())
|
||||
|
||||
return ResolvedMessage(msg, sender_id, sender_names, destination_id, destination_names)
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _resolve_name(self, name):
|
||||
# type: (str) -> (str, List[str])
|
||||
|
||||
if name is None:
|
||||
id = NONE
|
||||
names = []
|
||||
elif name.startswith(':'):
|
||||
id = name
|
||||
names = self.get_service_names_of_id(name)
|
||||
else:
|
||||
id = self.get_id_for_service_name(name)
|
||||
names = [name]
|
||||
|
||||
return id, names
|
||||
|
||||
def _on_name_owner_changed(self, msg):
|
||||
# type: (MatchedMessage) -> NoReturn
|
||||
|
||||
(name, old_id, new_id) = msg.arguments
|
||||
|
||||
old_id = old_id.strip()
|
||||
new_id = new_id.strip()
|
||||
name = name.strip()
|
||||
|
||||
if name.startswith(':'):
|
||||
name = None
|
||||
|
||||
added = old_id == '' and new_id != ''
|
||||
changed = old_id != '' and new_id != ''
|
||||
removed = old_id != '' and new_id == ''
|
||||
|
||||
# 'changed' is dispatched as 'removed' followed by 'added'
|
||||
|
||||
if removed or changed:
|
||||
self._services.pop(old_id, None)
|
||||
|
||||
if added or changed:
|
||||
service = self._create_service(name, new_id)
|
||||
self._services[new_id] = service
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _init_services(self):
|
||||
# type: () -> Dict[str, ServiceInfo]
|
||||
|
||||
services = dict()
|
||||
|
||||
names_and_ids = self._dbus.get_ids_and_service_names()
|
||||
|
||||
ids = set([i for i in names_and_ids if i.startswith(':')])
|
||||
names = [n for n in names_and_ids if not n.startswith(':')]
|
||||
|
||||
for service_name in names:
|
||||
service = self._create_service(service_name)
|
||||
services[service.id] = service
|
||||
ids.discard(service.id)
|
||||
|
||||
self._services = services # UGLY, because _create_service below references it.
|
||||
|
||||
for id in ids:
|
||||
services[id] = self._create_service(id=id)
|
||||
|
||||
return services
|
||||
|
||||
def _search_service_name_by_pid(self, pid):
|
||||
# type: (int) -> Optional[AnyStr]
|
||||
return next((s.name for s in self.services if s.pid == pid and s.name != NONE), NONE)
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
def _create_service(self, name=None, id=None):
|
||||
# type: (Optional[AnyStr], Optional[AnyStr]) -> ServiceInfo
|
||||
|
||||
id = id or self._dbus.get_id_of_service(name)
|
||||
pid = self._dbus.get_pid_of_service(id)
|
||||
proc = self._get_process_name_of_pid(pid)
|
||||
cmd = self._get_commandline_of_pid(pid)
|
||||
name = name or self._search_service_name_by_pid(pid)
|
||||
|
||||
return ServiceInfo(name, id, pid, proc, cmd)
|
||||
|
||||
# noinspection PyBroadException
|
||||
@staticmethod
|
||||
def _get_process_name_of_pid(service_pid):
|
||||
# type: (int) -> str
|
||||
|
||||
try:
|
||||
with open('/proc/{0}/comm'.format(service_pid)) as proc:
|
||||
return proc.read().replace('\0', ' ').rstrip()
|
||||
except Exception as _:
|
||||
return '<unknown>'
|
||||
|
||||
# noinspection PyBroadException
|
||||
@staticmethod
|
||||
def _get_commandline_of_pid(service_pid):
|
||||
# type: (int) -> str
|
||||
|
||||
try:
|
||||
with open('/proc/{0}/cmdline'.format(service_pid)) as proc:
|
||||
return proc.read().replace('\0', ' ').rstrip()
|
||||
except Exception as _:
|
||||
return '<unknown>'
|
Binary file not shown.
|
@ -0,0 +1,139 @@
|
|||
from logging import getLogger
|
||||
|
||||
import dbus
|
||||
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Any, Union, Dict
|
||||
DbusString = Union[dbus.String, dbus.UTF8String, dbus.ObjectPath, dbus.Signature]
|
||||
DbusInt = Union[dbus.Int16, dbus.Int32, dbus.Int64]
|
||||
DbusDouble = dbus.Double
|
||||
DbusBool = dbus.Boolean
|
||||
|
||||
DbusStringVariant = DbusString # TODO: variant_level constraint ?
|
||||
DbusIntVariant = DbusInt
|
||||
DbusDoubleVariant = DbusDouble
|
||||
DbusBoolVariant = DbusBool
|
||||
|
||||
DbusValue = Union[DbusString, DbusInt, DbusDouble, DbusBool, DBUS_NONE]
|
||||
DbusVariant = Union[DbusStringVariant, DbusIntVariant, DbusDoubleVariant, DbusBoolVariant, DBUS_NONE]
|
||||
|
||||
DbusTextDict = dbus.Dictionary
|
||||
DbusVariantDict = dbus.Dictionary
|
||||
|
||||
DbusType = Union[DbusValue, DbusVariant, DbusVariantDict, DbusTextDict]
|
||||
|
||||
DBUS_NONE = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) # DEFINED by victron
|
||||
|
||||
MAX_INT16 = 2 ** 15 - 1
|
||||
MAX_INT32 = 2 ** 31 - 1
|
||||
|
||||
|
||||
def dbus_uint32(value):
|
||||
# type: (int) -> dbus.UInt32
|
||||
if value < 0:
|
||||
raise Exception('cannot convert negative value to UInt32')
|
||||
|
||||
return dbus.UInt32(value)
|
||||
|
||||
|
||||
def dbus_int(value):
|
||||
# type: (Union[int, long]) -> Union[dbus.Int16, dbus.Int32, dbus.Int64]
|
||||
abs_value = abs(value)
|
||||
if abs_value < MAX_INT16:
|
||||
return dbus.Int16(value)
|
||||
elif abs_value < MAX_INT32:
|
||||
return dbus.Int32(value)
|
||||
else:
|
||||
return dbus.Int64(value)
|
||||
|
||||
|
||||
def dbus_string(value):
|
||||
# type: (Union[str, unicode]) -> DbusString
|
||||
if isinstance(value, unicode):
|
||||
return dbus.UTF8String(value)
|
||||
else:
|
||||
return dbus.String(value)
|
||||
|
||||
|
||||
def dbus_double(value):
|
||||
# type: (float) -> DbusDouble
|
||||
return dbus.Double(value)
|
||||
|
||||
|
||||
def dbus_bool(value):
|
||||
# type: (bool) -> DbusBool
|
||||
return dbus.Boolean(value)
|
||||
|
||||
|
||||
# VARIANTS
|
||||
|
||||
def dbus_int_variant(value):
|
||||
# type: (Union[int, long]) -> DbusIntVariant
|
||||
abs_value = abs(value)
|
||||
if abs_value < MAX_INT16:
|
||||
return dbus.Int16(value, variant_level=1)
|
||||
elif abs_value < MAX_INT32:
|
||||
return dbus.Int32(value, variant_level=1)
|
||||
else:
|
||||
return dbus.Int64(value, variant_level=1)
|
||||
|
||||
|
||||
def dbus_string_variant(value):
|
||||
# type: (Union[str, unicode]) -> DbusStringVariant
|
||||
if isinstance(value, unicode):
|
||||
return dbus.UTF8String(value, variant_level=1)
|
||||
else:
|
||||
return dbus.String(value, variant_level=1)
|
||||
|
||||
|
||||
def dbus_double_variant(value):
|
||||
# type: (float) -> DbusDoubleVariant
|
||||
return dbus.Double(value, variant_level=1)
|
||||
|
||||
|
||||
def dbus_bool_variant(value):
|
||||
# type: (bool) -> DbusBoolVariant
|
||||
return dbus.Boolean(value, variant_level=1)
|
||||
|
||||
|
||||
def dbus_variant(value):
|
||||
# type: (Any) -> DbusVariant
|
||||
|
||||
if value is None:
|
||||
return DBUS_NONE
|
||||
if isinstance(value, float):
|
||||
return dbus_double_variant(value)
|
||||
if isinstance(value, bool):
|
||||
return dbus_bool_variant(value)
|
||||
if isinstance(value, (int, long)):
|
||||
return dbus_int_variant(value)
|
||||
if isinstance(value, (str, unicode)):
|
||||
return dbus_string_variant(value)
|
||||
# TODO: container types
|
||||
|
||||
raise TypeError('unsupported python type: ' + str(type(value)) + ' ' + str(value))
|
||||
|
||||
|
||||
def dbus_value(value):
|
||||
# type: (Any) -> DbusVariant
|
||||
|
||||
if value is None:
|
||||
return DBUS_NONE
|
||||
if isinstance(value, float):
|
||||
return dbus_double(value)
|
||||
if isinstance(value, bool):
|
||||
return dbus_bool(value)
|
||||
if isinstance(value, (int, long)):
|
||||
return dbus_int(value)
|
||||
if isinstance(value, (str, unicode)):
|
||||
return dbus_string_variant(value)
|
||||
# TODO: container types
|
||||
|
||||
raise TypeError('unsupported python type: ' + str(type(value)) + ' ' + str(value))
|
||||
|
||||
|
||||
|
Binary file not shown.
|
@ -0,0 +1,259 @@
|
|||
from fnmatch import fnmatch as glob
|
||||
from logging import getLogger
|
||||
|
||||
from _dbus_bindings import ErrorMessage, Message, MethodReturnMessage
|
||||
from python_libs.ie_utils.mixins import Record
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import List, Optional, Iterable, AnyStr, NoReturn, Any
|
||||
|
||||
|
||||
class MessageType(object):
|
||||
|
||||
invalid = 0
|
||||
method_call = 1
|
||||
method_return = 2
|
||||
error = 3
|
||||
signal = 4
|
||||
|
||||
@staticmethod
|
||||
def parse(message_type):
|
||||
# type: (int) -> str
|
||||
|
||||
if message_type == 1:
|
||||
return 'method_call'
|
||||
if message_type == 2:
|
||||
return 'method_return'
|
||||
if message_type == 3:
|
||||
return 'error'
|
||||
if message_type == 4:
|
||||
return 'signal'
|
||||
|
||||
return 'invalid'
|
||||
|
||||
|
||||
class DBusMessage(Record):
|
||||
|
||||
def __init__(self, msg, sender_id, destination_id):
|
||||
# type: (Message, str, str) -> NoReturn
|
||||
|
||||
self.sender_id = sender_id
|
||||
self.destination_id = destination_id
|
||||
self._msg = msg
|
||||
|
||||
@property
|
||||
def expects_reply(self):
|
||||
# type: () -> bool
|
||||
return not self._msg.get_no_reply()
|
||||
|
||||
@property
|
||||
def message_type(self):
|
||||
# type: () -> int
|
||||
return int(self._msg.get_type())
|
||||
|
||||
@property
|
||||
def reply_serial(self):
|
||||
# type: () -> int
|
||||
return int(self._msg.get_reply_serial())
|
||||
|
||||
@property
|
||||
def object_path(self):
|
||||
# type: () -> str
|
||||
return str(self._msg.get_path())
|
||||
|
||||
@property
|
||||
def interface(self):
|
||||
# type: () -> str
|
||||
return str(self._msg.get_interface())
|
||||
|
||||
@property
|
||||
def arguments(self):
|
||||
# type: () -> List[Any]
|
||||
return self._msg.get_args_list(utf8_strings=True)
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
# type: () -> str
|
||||
return str(self._msg.get_signature())
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
# type: () -> int
|
||||
return int(self._msg.get_serial())
|
||||
|
||||
@property
|
||||
def member(self):
|
||||
# type: () -> str
|
||||
return str(self._msg.get_member())
|
||||
|
||||
def create_method_reply(self, *args):
|
||||
# type: (List[any]) -> MethodReturnMessage
|
||||
|
||||
if self.message_type != MessageType.method_call:
|
||||
raise Exception('cannot create a reply for a message that is not a method call')
|
||||
|
||||
reply = MethodReturnMessage(self._msg)
|
||||
|
||||
for arg in args:
|
||||
reply.append(arg)
|
||||
|
||||
return reply
|
||||
|
||||
def create_error_reply(self, exception):
|
||||
# type: (Exception) -> ErrorMessage
|
||||
|
||||
if self.message_type != MessageType.method_call:
|
||||
raise Exception('cannot create an error reply for a message that is not a method call')
|
||||
|
||||
return ErrorMessage(self._msg, 'com.victronenergy.' + exception.__class__.__name__, exception.message) # TODO prefix
|
||||
|
||||
|
||||
class ResolvedMessage(DBusMessage):
|
||||
|
||||
def __init__(self, msg, sender_id, sender_names, destination_id, destination_names):
|
||||
# type: (Message, str, List[str], str, List[str]) -> NoReturn
|
||||
|
||||
super(ResolvedMessage, self).__init__(msg, sender_id, destination_id)
|
||||
|
||||
self.sender_names = sender_names
|
||||
self.destination_names = destination_names
|
||||
|
||||
|
||||
class MatchedMessage(DBusMessage):
|
||||
|
||||
def __init__(self, resolved_msg, sender_name, destination_name):
|
||||
# type: (ResolvedMessage, str, str) -> NoReturn
|
||||
|
||||
super(MatchedMessage, self).__init__(resolved_msg._msg, resolved_msg.sender_id, resolved_msg.destination_id)
|
||||
|
||||
self.sender_name = sender_name
|
||||
self.destination_name = destination_name
|
||||
|
||||
|
||||
class MessageFilter(Record):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message_type='*',
|
||||
sender_id='*',
|
||||
sender_name='*',
|
||||
object_path='*',
|
||||
interface='*',
|
||||
member='*',
|
||||
signature='*',
|
||||
destination_id='*',
|
||||
destination_name='*',
|
||||
eavesdrop=False):
|
||||
|
||||
# type: (Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[bool]) -> NoReturn
|
||||
|
||||
self.signature = signature
|
||||
self.message_type = message_type
|
||||
|
||||
self.member = member
|
||||
self.interface = interface
|
||||
self.object_path = object_path
|
||||
|
||||
self.sender_id = sender_id
|
||||
self.sender_name = sender_name
|
||||
self.destination_id = destination_id
|
||||
self.destination_name = destination_name
|
||||
|
||||
self.eavesdrop = eavesdrop
|
||||
|
||||
@staticmethod
|
||||
def create_filter_rule(
|
||||
message_type='*',
|
||||
sender_id='*',
|
||||
sender_name='*',
|
||||
object_path='*',
|
||||
interface='*',
|
||||
member='*',
|
||||
destination_id='*',
|
||||
eavesdrop=False):
|
||||
# type: (Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],Optional[AnyStr],bool) -> AnyStr
|
||||
|
||||
rules = []
|
||||
|
||||
def rule(key, value):
|
||||
if '*' not in value and '?' not in value:
|
||||
rules.append("%s='%s'" % (key, value))
|
||||
|
||||
rule('type', message_type)
|
||||
rule('sender', sender_id if sender_name == '*' and sender_id != '*' else sender_name)
|
||||
rule('destination', destination_id)
|
||||
rule('eavesdrop', 'true' if eavesdrop else 'false')
|
||||
rule('path', object_path) # TODO: endswith *, object namespace
|
||||
rule('interface', interface)
|
||||
rule('member', member)
|
||||
|
||||
return ','.join(rules)
|
||||
|
||||
@property
|
||||
def filter_rule(self):
|
||||
# type: () -> AnyStr
|
||||
|
||||
return self.create_filter_rule(
|
||||
message_type=self.message_type,
|
||||
sender_id=self.sender_id,
|
||||
sender_name=self.sender_name,
|
||||
object_path=self.object_path,
|
||||
interface=self.interface,
|
||||
member=self.member,
|
||||
destination_id=self.destination_id,
|
||||
eavesdrop=self.eavesdrop)
|
||||
|
||||
@staticmethod
|
||||
def _get_matching_name(names, name_filter):
|
||||
# type: (Iterable[AnyStr], AnyStr) -> Optional[AnyStr]
|
||||
|
||||
matching_names = (
|
||||
name
|
||||
for name
|
||||
in names
|
||||
if glob(name, name_filter)
|
||||
)
|
||||
|
||||
return next(matching_names, None)
|
||||
|
||||
def match_message(self, msg):
|
||||
# type: (ResolvedMessage) -> Optional[MatchedMessage]
|
||||
|
||||
match = \
|
||||
glob(msg.object_path, self.object_path) and \
|
||||
glob(msg.interface or '<none>', self.interface) and \
|
||||
glob(msg.member, self.member) and \
|
||||
glob(msg.signature, self.signature) and \
|
||||
glob(msg.sender_id, self.sender_id) and \
|
||||
glob(msg.destination_id or '<none>', self.destination_id)
|
||||
|
||||
if not match:
|
||||
return None
|
||||
|
||||
sender_name = self._get_matching_name(msg.sender_names, self.sender_name)
|
||||
if sender_name is None and self.sender_name != '*': # sender might not have a well known name
|
||||
return None
|
||||
|
||||
destination_name = self._get_matching_name(msg.destination_names, self.destination_name)
|
||||
if destination_name is None and self.destination_name != '*':
|
||||
return None
|
||||
|
||||
return MatchedMessage(msg, sender_name, destination_name)
|
||||
|
||||
|
||||
class DBusException(Exception):
|
||||
|
||||
def __init__(self, message):
|
||||
super(Exception, self).__init__(message)
|
||||
|
||||
@classmethod
|
||||
def raise_if_error_reply(cls, reply):
|
||||
# type: (Message) -> Message
|
||||
|
||||
if isinstance(reply, ErrorMessage):
|
||||
raise DBusException(reply.get_error_name())
|
||||
else:
|
||||
return reply
|
Binary file not shown.
|
@ -0,0 +1,177 @@
|
|||
|
||||
from logging import getLogger
|
||||
|
||||
import dbus
|
||||
|
||||
from python_libs.ie_dbus.private.dbus_types import dbus_variant, dbus_string
|
||||
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
|
||||
from python_libs.ie_dbus.private.message_types import MatchedMessage
|
||||
from python_libs.ie_dbus.private.ve_constants import GET_TEXT, INTERFACE_BUS_ITEM, PROPERTIES_CHANGED, GET_VALUE, SET_VALUE
|
||||
from python_libs.ie_utils.mixins import Disposable, Record
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Optional, AnyStr, NoReturn, Dict, Any
|
||||
from python_libs.ie_dbus.private.dbus_types import DbusVariant, DbusString, DbusVariantDict, DbusType
|
||||
|
||||
|
||||
class OwnProperty(Record):
|
||||
|
||||
def __init__(self, value, unit='', writable=False):
|
||||
|
||||
str_value = round(value, 2) if isinstance(value, float) else value
|
||||
|
||||
self.text = unicode(str_value) + unit
|
||||
self.value = value
|
||||
self.unit = unit
|
||||
self.writable = writable
|
||||
|
||||
@property
|
||||
def dbus_dict(self):
|
||||
# type: () -> dbus.Dictionary
|
||||
d = {
|
||||
dbus.String('Text'): dbus_variant(self.text),
|
||||
dbus.String('Value'): dbus_variant(self.value)
|
||||
}
|
||||
return dbus.Dictionary(d, signature='sv')
|
||||
|
||||
@property
|
||||
def dbus_value(self):
|
||||
# type: () -> DbusVariant
|
||||
return dbus_variant(self.value)
|
||||
|
||||
@property
|
||||
def dbus_text(self):
|
||||
# type: () -> DbusString
|
||||
return dbus_string(self.text)
|
||||
|
||||
def update_value(self, value):
|
||||
# type: (any) -> OwnProperty
|
||||
return OwnProperty(value, self.unit, self.writable)
|
||||
|
||||
def __iter__(self):
|
||||
yield self.value
|
||||
yield self.text
|
||||
|
||||
|
||||
class OwnProperties(Disposable):
|
||||
|
||||
_own_properties = None # type: Dict[AnyStr, OwnProperty]
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def __init__(self, daemon):
|
||||
# type: (DBusDaemon) -> NoReturn
|
||||
|
||||
self._daemon = daemon
|
||||
self._own_properties = dict()
|
||||
self._method_call_subs = self._daemon.subscribe_to_method_call_message(self._on_method_called) # no filter whatsoever
|
||||
|
||||
def get(self, object_path):
|
||||
# type: (AnyStr) -> OwnProperty
|
||||
return self._own_properties[object_path]
|
||||
|
||||
def set(self, object_path, value, unit='', writable=False):
|
||||
# type: (AnyStr, any, Optional[AnyStr], Optional[bool]) -> bool
|
||||
|
||||
prop = OwnProperty(value, unit, writable)
|
||||
|
||||
if object_path in self._own_properties:
|
||||
if self._own_properties[object_path] == prop:
|
||||
return False
|
||||
|
||||
self._own_properties[object_path] = prop
|
||||
# object_path, interface, member, *args):
|
||||
self._daemon.broadcast_signal(
|
||||
object_path,
|
||||
INTERFACE_BUS_ITEM,
|
||||
PROPERTIES_CHANGED,
|
||||
prop.dbus_dict)
|
||||
|
||||
return True
|
||||
|
||||
def _on_method_called(self, message):
|
||||
# type: (MatchedMessage) -> Any
|
||||
|
||||
# _log.info(str(message.sender_name) + '(' + str(message.sender_id) + ') asked ' + message.member + ' ' + message.object_path)
|
||||
|
||||
if message.member == GET_VALUE:
|
||||
return self._on_get_value_called(message)
|
||||
elif message.member == GET_TEXT:
|
||||
return self._on_get_text_called(message)
|
||||
elif message.member == SET_VALUE:
|
||||
return self._on_set_value_called(message)
|
||||
|
||||
def _on_set_value_called(self, message):
|
||||
# type: (MatchedMessage) -> bool
|
||||
|
||||
path = message.object_path
|
||||
|
||||
if path not in self._own_properties:
|
||||
raise Exception('property ' + path + ' does not exist')
|
||||
|
||||
prop = self._own_properties[path]
|
||||
if not prop.writable:
|
||||
raise Exception('property ' + path + ' is read-only')
|
||||
|
||||
value = message.arguments[0]
|
||||
|
||||
if prop.value == value:
|
||||
return False
|
||||
|
||||
prop = prop.update_value(value)
|
||||
self._own_properties[path] = prop
|
||||
|
||||
# object_path, interface, member, *args):
|
||||
self._daemon.broadcast_signal(
|
||||
path,
|
||||
INTERFACE_BUS_ITEM,
|
||||
PROPERTIES_CHANGED,
|
||||
prop.dbus_dict)
|
||||
|
||||
return True
|
||||
|
||||
def _on_get_value_called(self, message):
|
||||
# type: (MatchedMessage) -> DbusType
|
||||
|
||||
path = message.object_path
|
||||
|
||||
if path in self._own_properties:
|
||||
return self._own_properties[path].dbus_value
|
||||
|
||||
if path.endswith('/'): # "Tree Export"
|
||||
values = {
|
||||
dbus.String(k.lstrip('/')): dbus_variant(p.value)
|
||||
for (k, p)
|
||||
in self._own_properties.iteritems()
|
||||
if k.startswith(path)
|
||||
}
|
||||
|
||||
return dbus.Dictionary(values, signature='sv', variant_level=1) # variant for tree export !!
|
||||
|
||||
raise Exception('property ' + path + ' does not exist')
|
||||
|
||||
def _on_get_text_called(self, message):
|
||||
# type: (MatchedMessage) -> DbusType
|
||||
|
||||
path = message.object_path
|
||||
|
||||
if path in self._own_properties:
|
||||
return self._own_properties[message.object_path].dbus_text
|
||||
|
||||
if path.endswith('/'): # "Tree Export"
|
||||
values = {
|
||||
dbus.String(k.lstrip('/')): dbus.String(p.text)
|
||||
for (k, p)
|
||||
in self._own_properties.iteritems()
|
||||
if k.startswith(path)
|
||||
}
|
||||
return dbus.Dictionary(values, signature='ss', variant_level=1) # variant for tree export !!
|
||||
|
||||
raise Exception('property ' + path + ' does not exist')
|
||||
|
||||
def __contains__(self, object_path):
|
||||
# type: (AnyStr) -> bool
|
||||
return object_path in self._own_properties
|
Binary file not shown.
|
@ -0,0 +1,166 @@
|
|||
from logging import getLogger
|
||||
|
||||
from python_libs.ie_dbus.private.dbus_types import dbus_variant
|
||||
from python_libs.ie_utils.mixins import Disposable, Record
|
||||
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
|
||||
from python_libs.ie_dbus.private.message_types import MatchedMessage
|
||||
from python_libs.ie_dbus.private.ve_constants import GET_TEXT, INTERFACE_BUS_ITEM, PROPERTIES_CHANGED, GET_VALUE, SERVICE_PREFIX, SET_VALUE
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
_UNKNOWN_TEXT = '<UNKNOWN_TEXT>'
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import List, AnyStr, NoReturn, Dict, Any
|
||||
|
||||
|
||||
class RemoteProperty(Record):
|
||||
|
||||
def __init__(self, value, text):
|
||||
|
||||
self.text = text
|
||||
self.value = value
|
||||
|
||||
@staticmethod
|
||||
def from_dbus_dict(dbus_dict):
|
||||
value = dbus_dict['Value']
|
||||
text = dbus_dict['Text']
|
||||
return RemoteProperty(value, text)
|
||||
|
||||
|
||||
class RemoteProperties(Disposable):
|
||||
|
||||
_remote_properties = None # type: Dict[AnyStr, RemoteProperty]
|
||||
|
||||
def __init__(self, daemon):
|
||||
# type: (DBusDaemon) -> NoReturn
|
||||
|
||||
self._daemon = daemon
|
||||
self._remote_properties = dict()
|
||||
|
||||
# noinspection PyBroadException
|
||||
def available_properties(self, service_name):
|
||||
# type: (unicode) -> List[unicode]
|
||||
|
||||
if not self._daemon.exists_service_with_name(service_name):
|
||||
return []
|
||||
|
||||
try:
|
||||
paths = self._call_remote(service_name=service_name, object_path='/', member=GET_TEXT)[0].keys()
|
||||
except Exception as _:
|
||||
return []
|
||||
else:
|
||||
return ['/' + str(path) for path in paths]
|
||||
|
||||
def exists(self, combined_path):
|
||||
# type: (AnyStr) -> bool
|
||||
|
||||
service_name, object_path, combined_path = self._parse_combined_path(combined_path)
|
||||
return object_path in self.available_properties(service_name)
|
||||
|
||||
def get(self, combined_path):
|
||||
# type: (AnyStr) -> RemoteProperty
|
||||
|
||||
service_name, object_path, combined_path = self._parse_combined_path(combined_path)
|
||||
|
||||
if combined_path in self._remote_properties:
|
||||
cached = self._remote_properties[combined_path]
|
||||
|
||||
# a cached prop might have an unknown text, because its value has been written before,
|
||||
# but it has never read or updated via property-changed
|
||||
|
||||
if cached.text != _UNKNOWN_TEXT:
|
||||
return cached
|
||||
|
||||
text = self._get_text(service_name, object_path)
|
||||
self._remote_properties[combined_path] = RemoteProperty(cached.value, text)
|
||||
|
||||
return self._remote_properties[combined_path]
|
||||
|
||||
prop = self._get_property(service_name, object_path)
|
||||
self._remote_properties[combined_path] = prop
|
||||
self._subscribe_to_property_changed(service_name, object_path)
|
||||
|
||||
return prop
|
||||
|
||||
def set(self, combined_path, value):
|
||||
# type: (AnyStr, any) -> bool
|
||||
|
||||
service_name, object_path, combined_path = self._parse_combined_path(combined_path)
|
||||
|
||||
if combined_path in self._remote_properties:
|
||||
if self._remote_properties[combined_path].value == value:
|
||||
return False # property already has the requested value => nothing to do
|
||||
else:
|
||||
self._subscribe_to_property_changed(service_name, object_path)
|
||||
|
||||
result = self._call_remote(service_name, object_path, SET_VALUE, dbus_variant(value))[0]
|
||||
|
||||
if result != 0:
|
||||
raise Exception(service_name + ' refused to set value of ' + object_path + ' to ' + str(value))
|
||||
|
||||
self._remote_properties[combined_path] = RemoteProperty(value, _UNKNOWN_TEXT)
|
||||
|
||||
return True
|
||||
|
||||
def _subscribe_to_property_changed(self, service_name, object_path):
|
||||
# type: (unicode, unicode) -> NoReturn
|
||||
|
||||
def callback(msg):
|
||||
# type: (MatchedMessage) -> NoReturn
|
||||
prop = RemoteProperty.from_dbus_dict(msg.arguments[0])
|
||||
key = msg.sender_name+msg.object_path
|
||||
self._remote_properties[key] = prop
|
||||
|
||||
signal = self._daemon.subscribe_to_signal_message(
|
||||
callback=callback,
|
||||
sender_name=service_name,
|
||||
object_path=object_path,
|
||||
interface=INTERFACE_BUS_ITEM, # TODO: <- this could be removed to make it more robust, in theory
|
||||
member=PROPERTIES_CHANGED) # TODO: OTOH, don't fix if it is not broken
|
||||
|
||||
self.chain_disposable(signal, 'signal subscription on ' + self._daemon.bus_id + ' ' + service_name + object_path)
|
||||
|
||||
def _get_value(self, service_name, object_path):
|
||||
# type: (unicode, unicode) -> any
|
||||
|
||||
return self._call_remote(service_name, object_path, GET_VALUE)[0]
|
||||
|
||||
def _get_text(self, service_name, object_path):
|
||||
# type: (unicode, unicode) -> unicode
|
||||
|
||||
result = self._call_remote(service_name, object_path, GET_TEXT)[0]
|
||||
return unicode(result)
|
||||
|
||||
def _get_property(self, service_name, object_path):
|
||||
# type: (unicode, unicode) -> RemoteProperty
|
||||
|
||||
value = self._get_value(service_name, object_path)
|
||||
text = self._get_text(service_name, object_path)
|
||||
|
||||
return RemoteProperty(value, text)
|
||||
|
||||
def _call_remote(self, service_name, object_path, member, *args):
|
||||
# type: (unicode, unicode, unicode, List[Any]) -> List[Any]
|
||||
|
||||
return self._daemon.call_method(service_name, object_path, INTERFACE_BUS_ITEM, member, *args)
|
||||
|
||||
def _parse_combined_path(self, combined_path):
|
||||
# type: (str) -> (unicode,unicode,unicode)
|
||||
|
||||
service_name, object_path = combined_path.lstrip('/').split('/', 1)
|
||||
|
||||
if service_name == '':
|
||||
raise Exception('Failed to parse service name. \ncombined_path must be of the form "service_name/path/to/property"')
|
||||
if object_path == '':
|
||||
raise Exception('Failed to parse object path. \ncombined_path must be of the form "service_name/path/to/property"')
|
||||
|
||||
service_name = service_name if service_name.startswith(SERVICE_PREFIX) else SERVICE_PREFIX + service_name
|
||||
|
||||
if not self._daemon.exists_service_with_name(service_name):
|
||||
raise Exception('there is no service with the name "' + service_name + '" on the bus')
|
||||
|
||||
object_path = '/' + object_path
|
||||
|
||||
return unicode(service_name), unicode(object_path), unicode(service_name + object_path)
|
Binary file not shown.
|
@ -0,0 +1,89 @@
|
|||
from logging import getLogger
|
||||
|
||||
from python_libs.ie_dbus.private.dbus_types import dbus_string, dbus_int_variant, dbus_string_variant, dbus_double_variant, dbus_variant
|
||||
from python_libs.ie_utils.mixins import Record
|
||||
from python_libs.ie_dbus.private.dbus_daemon import DBusDaemon
|
||||
from python_libs.ie_dbus.private.remote_properties import RemoteProperties
|
||||
from python_libs.ie_dbus.private.ve_constants import SETTINGS_SERVICE, SETTINGS_INTERFACE, SETTINGS_PREFIX
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Union, NoReturn, Optional, AnyStr
|
||||
|
||||
|
||||
def prepend_settings_prefix(path):
|
||||
# type: (AnyStr) -> any
|
||||
|
||||
path = '/' + path.lstrip('/')
|
||||
path = path if path.startswith(SETTINGS_PREFIX) else SETTINGS_PREFIX + path
|
||||
return path
|
||||
|
||||
|
||||
class Settings(Record):
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def __init__(self, daemon, remote_properties):
|
||||
# type: (DBusDaemon, RemoteProperties) -> NoReturn
|
||||
|
||||
self._daemon = daemon
|
||||
self._remote_properties = remote_properties
|
||||
|
||||
# noinspection PyShadowingBuiltins
|
||||
|
||||
def add_setting(self, path, default_value, min=None, max=None, silent=False):
|
||||
# type: (AnyStr, Union[unicode, int, float], Union[int, float, None], Union[int, float, None], Optional[bool]) -> NoReturn
|
||||
|
||||
path = prepend_settings_prefix(path)
|
||||
|
||||
if isinstance(default_value, int):
|
||||
item_type = 'i'
|
||||
elif isinstance(default_value, float):
|
||||
item_type = 'f'
|
||||
elif isinstance(default_value, (str, unicode)):
|
||||
item_type = 's'
|
||||
else:
|
||||
raise Exception('Unsupported Settings Type')
|
||||
|
||||
reply = self._daemon.call_method(
|
||||
SETTINGS_SERVICE, # service_name
|
||||
'/', # object_path
|
||||
SETTINGS_INTERFACE, # interface
|
||||
'AddSilentSetting' if silent else 'AddSetting', # member,
|
||||
dbus_string(''), # "group", not used
|
||||
dbus_string(path),
|
||||
dbus_variant(default_value),
|
||||
dbus_string(item_type),
|
||||
dbus_int_variant(min or 0),
|
||||
dbus_int_variant(max or 0))
|
||||
|
||||
if reply[0] != 0:
|
||||
raise Exception('failed to add setting ' + path)
|
||||
|
||||
def exists(self, path):
|
||||
# type: (unicode) -> bool
|
||||
|
||||
path = prepend_settings_prefix(path)
|
||||
return path in self.available_settings
|
||||
|
||||
def get(self, path):
|
||||
# type: (unicode) -> Union[unicode, int, float]
|
||||
|
||||
path = prepend_settings_prefix(path)
|
||||
return self._remote_properties.get(SETTINGS_SERVICE + path).value
|
||||
|
||||
def set(self, path, value):
|
||||
# type: (unicode, Union[unicode, int, float]) -> NoReturn
|
||||
|
||||
path = prepend_settings_prefix(path)
|
||||
self._remote_properties.set(SETTINGS_SERVICE + path, value)
|
||||
|
||||
@property
|
||||
def available_settings(self):
|
||||
# type: () -> [unicode]
|
||||
return self._remote_properties.available_properties(SETTINGS_SERVICE)
|
||||
|
||||
def __contains__(self, path):
|
||||
# type: (unicode) -> bool
|
||||
return self.exists(path)
|
Binary file not shown.
|
@ -0,0 +1,11 @@
|
|||
|
||||
SERVICE_PREFIX = 'com.victronenergy.'
|
||||
VE_SERVICE_FILTER = SERVICE_PREFIX + '*'
|
||||
INTERFACE_BUS_ITEM = SERVICE_PREFIX + 'BusItem'
|
||||
PROPERTIES_CHANGED = 'PropertiesChanged'
|
||||
GET_VALUE = 'GetValue'
|
||||
SET_VALUE = 'SetValue'
|
||||
GET_TEXT = 'GetText'
|
||||
SETTINGS_SERVICE = 'com.victronenergy.settings'
|
||||
SETTINGS_INTERFACE = 'com.victronenergy.Settings'
|
||||
SETTINGS_PREFIX = '/Settings'
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,73 @@
|
|||
from logging import getLogger
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import NoReturn, Optional
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
|
||||
class MovingAverageFilter(object):
|
||||
|
||||
def __init__(self, length=30, initial_value=0):
|
||||
# type: (int, float) -> NoReturn
|
||||
|
||||
self.value = initial_value
|
||||
self.length = length
|
||||
|
||||
def update(self, value, length=None):
|
||||
# type: (float, int) -> float
|
||||
|
||||
if length is not None:
|
||||
self.length = length
|
||||
|
||||
self.value = (self.value * self.length + value) / (self.length + 1)
|
||||
|
||||
_log.debug('real value: ' + str(value) + ', filtered value: ' + str(self.value))
|
||||
|
||||
return self.value
|
||||
|
||||
|
||||
class DebounceFilter(object):
|
||||
|
||||
def __init__(self, initial_state=None, max_inertia=10):
|
||||
# type: (Optional[bool], Optional[int]) -> NoReturn
|
||||
|
||||
self._max_inertia = max_inertia
|
||||
self._inertia = max_inertia
|
||||
self._state = initial_state
|
||||
|
||||
def reset(self, state=None, max_inertia=None):
|
||||
# type: (Optional[bool], Optional[int]) -> bool
|
||||
|
||||
self._max_inertia = max_inertia or self._max_inertia
|
||||
self._inertia = self._max_inertia
|
||||
self._state = state or self._state
|
||||
|
||||
_log.debug('debounce filter reset: state={0}, inertia={1}'.format(self._state, self._inertia))
|
||||
|
||||
return self._state
|
||||
|
||||
def flip(self):
|
||||
# type: () -> bool
|
||||
self._state = not self._state
|
||||
self._inertia = self._max_inertia
|
||||
return self._state
|
||||
|
||||
def update(self, new_state, max_inertia=None):
|
||||
# type: (bool, int) -> bool
|
||||
|
||||
if max_inertia is not None and max_inertia != self._max_inertia:
|
||||
return self.reset(new_state, max_inertia)
|
||||
|
||||
if new_state != self._state:
|
||||
if self._inertia > 0:
|
||||
self._inertia = self._inertia - 1
|
||||
else:
|
||||
self.flip()
|
||||
else:
|
||||
self._inertia = min(self._inertia + 1, self._max_inertia)
|
||||
|
||||
_log.debug('debounce filter update: state={0}, inertia={1}'.format(self._state, self._inertia))
|
||||
|
||||
return self._state
|
Binary file not shown.
|
@ -0,0 +1,30 @@
|
|||
from logging import getLogger
|
||||
import traceback
|
||||
import gobject
|
||||
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, NoReturn
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
|
||||
def run_on_main_loop(update_action, update_period):
|
||||
# type: (Callable[[],NoReturn], int) -> NoReturn
|
||||
|
||||
main_loop = gobject.MainLoop()
|
||||
|
||||
def update(*args, **kwargs):
|
||||
try:
|
||||
update_action()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
_log.error(e.message)
|
||||
traceback.print_exc()
|
||||
main_loop.quit()
|
||||
return False
|
||||
|
||||
gobject.timeout_add(update_period, update)
|
||||
main_loop.run()
|
Binary file not shown.
|
@ -0,0 +1,115 @@
|
|||
from logging import getLogger
|
||||
from _dbus_glib_bindings import DBusGMainLoop
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable, NoReturn, List, AnyStr, Optional, Union
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
|
||||
def nop(*_args):
|
||||
pass
|
||||
|
||||
|
||||
def memoize(fn):
|
||||
|
||||
attr_name = '_memoized_' + fn.__name__
|
||||
|
||||
def _memoized(self):
|
||||
if not hasattr(self, attr_name):
|
||||
setattr(self, attr_name, fn(self))
|
||||
return getattr(self, attr_name)
|
||||
|
||||
return _memoized
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
class Disposable(object):
|
||||
|
||||
_dispose_actions = None # type: List[Callable[[],NoReturn]]
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, typ, value, tb):
|
||||
self.dispose()
|
||||
|
||||
def dispose(self):
|
||||
# type: () -> NoReturn
|
||||
|
||||
while self._dispose_actions:
|
||||
dispose = self._dispose_actions.pop()
|
||||
dispose()
|
||||
|
||||
for k, v in self.__dict__.iteritems():
|
||||
if isinstance(v, Disposable) and v._dispose_actions:
|
||||
_log.debug('disposing ' + type(self).__name__ + '.' + k)
|
||||
v.dispose()
|
||||
|
||||
def chain_disposable(self, dispose, message=None):
|
||||
# type: (Union[Callable[[],None],Disposable], Optional[AnyStr]) -> NoReturn
|
||||
|
||||
if self._dispose_actions is None:
|
||||
self._dispose_actions = []
|
||||
|
||||
if isinstance(dispose, Disposable):
|
||||
dispose = dispose.dispose
|
||||
|
||||
if message is None:
|
||||
self._dispose_actions.append(dispose)
|
||||
return
|
||||
|
||||
def dispose_with_log_msg():
|
||||
_log.debug('disposing ' + message)
|
||||
dispose()
|
||||
|
||||
# _log.debug('new disposable ' + message)
|
||||
self._dispose_actions.append(dispose_with_log_msg)
|
||||
|
||||
@classmethod
|
||||
def create(cls, dispose_action, message=None):
|
||||
# type: (Union[Callable[[],None],Disposable], Optional[AnyStr]) -> Disposable
|
||||
|
||||
disposable = Disposable()
|
||||
disposable.chain_disposable(dispose_action, message)
|
||||
return disposable
|
||||
|
||||
def create_dependent_disposable(self, dispose_action, message=None):
|
||||
# type: (Union[Callable[[],None],Disposable], Optional[AnyStr]) -> Disposable
|
||||
|
||||
disposable = Disposable.create(dispose_action, message)
|
||||
self.chain_disposable(disposable)
|
||||
return disposable
|
||||
|
||||
|
||||
class Record(object):
|
||||
|
||||
@memoize
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + ' ' + unicode(vars(self))
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@memoize
|
||||
def __hash__(self):
|
||||
return self.__str__().__hash__()
|
||||
|
||||
def __eq__(self, other):
|
||||
# TODO: improve, iterable vars are not correctly handled
|
||||
return str(other) == str(self)
|
||||
|
||||
# make readonly
|
||||
def __setattr__(self, key, value):
|
||||
# type: (str, any) -> NoReturn
|
||||
|
||||
if not key.startswith('_') and hasattr(self, key): # disallow redefining
|
||||
raise ValueError(key + ' is read-only' + str(dir()))
|
||||
|
||||
super(Record, self).__setattr__(key, value)
|
||||
|
||||
|
||||
class RequiresMainLoop(object):
|
||||
|
||||
main_loop = DBusGMainLoop(set_as_default=True) # initialized only once for all subclasses that need it
|
Binary file not shown.
|
@ -0,0 +1,44 @@
|
|||
from logging import getLogger
|
||||
import re
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Dict
|
||||
|
||||
_log = getLogger(__name__)
|
||||
|
||||
|
||||
def make2way(dic):
|
||||
# type: (Dict) -> Dict
|
||||
for k, v in dic.items():
|
||||
dic[v] = k
|
||||
|
||||
return dic
|
||||
|
||||
|
||||
def invert_dict(src_dic):
|
||||
# type: (Dict) -> Dict
|
||||
dic = dict()
|
||||
|
||||
for k, v in src_dic.items():
|
||||
dic[v] = k
|
||||
|
||||
return dic
|
||||
|
||||
|
||||
def enum_file_name_of(path):
|
||||
# type: (str) -> Dict[int,str]
|
||||
|
||||
"""
|
||||
This is kinda hacky, but it works :)
|
||||
The enum file must contain a single enum however!
|
||||
"""
|
||||
|
||||
path = path[0:-1] if path.endswith('.pyc') else path
|
||||
pattern = re.compile(r"^\s*(\w+)\s*=\s*(\d+)", re.M)
|
||||
with open(path, "r") as f:
|
||||
return {
|
||||
int(m[1]): m[0]
|
||||
for m
|
||||
in pattern.findall(f.read())
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2019 Ram Rachum and collaborators.
|
||||
# This program is distributed under the MIT license.
|
||||
'''
|
||||
PySnooper - Never use print for debugging again
|
||||
|
||||
Usage:
|
||||
|
||||
import pysnooper
|
||||
|
||||
@pysnooper.snoop()
|
||||
def your_function(x):
|
||||
...
|
||||
|
||||
A log will be written to stderr showing the lines executed and variables
|
||||
changed in the decorated function.
|
||||
|
||||
For more information, see https://github.com/cool-RR/PySnooper
|
||||
'''
|
||||
|
||||
from .tracer import Tracer as snoop
|
||||
from .variables import Attrs, Exploding, Indices, Keys
|
||||
import collections
|
||||
|
||||
__VersionInfo = collections.namedtuple('VersionInfo',
|
||||
('major', 'minor', 'micro'))
|
||||
|
||||
__version__ = '0.4.0'
|
||||
__version_info__ = __VersionInfo(*(map(int, __version__.split('.'))))
|
||||
|
||||
del collections, __VersionInfo # Avoid polluting the namespace
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright 2019 Ram Rachum and collaborators.
|
||||
# This program is distributed under the MIT license.
|
||||
'''Python 2/3 compatibility'''
|
||||
|
||||
import abc
|
||||
import os
|
||||
import inspect
|
||||
import sys
|
||||
import datetime as datetime_module
|
||||
|
||||
PY3 = (sys.version_info[0] == 3)
|
||||
PY2 = not PY3
|
||||
|
||||
if hasattr(abc, 'ABC'):
|
||||
ABC = abc.ABC
|
||||
else:
|
||||
class ABC(object):
|
||||
"""Helper class that provides a standard way to create an ABC using
|
||||
inheritance.
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
if hasattr(os, 'PathLike'):
|
||||
PathLike = os.PathLike
|
||||
else:
|
||||
class PathLike(ABC):
|
||||
"""Abstract base class for implementing the file system path protocol."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __fspath__(self):
|
||||
"""Return the file system path representation of the object."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, subclass):
|
||||
return (
|
||||
hasattr(subclass, '__fspath__') or
|
||||
# Make a concession for older `pathlib` versions:g
|
||||
(hasattr(subclass, 'open') and
|
||||
'path' in subclass.__name__.lower())
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
iscoroutinefunction = inspect.iscoroutinefunction
|
||||
except AttributeError:
|
||||
iscoroutinefunction = lambda whatever: False # Lolz
|
||||
|
||||
try:
|
||||
isasyncgenfunction = inspect.isasyncgenfunction
|
||||
except AttributeError:
|
||||
isasyncgenfunction = lambda whatever: False # Lolz
|
||||
|
||||
|
||||
if PY3:
|
||||
string_types = (str,)
|
||||
text_type = str
|
||||
else:
|
||||
string_types = (basestring,)
|
||||
text_type = unicode
|
||||
|
||||
try:
|
||||
from collections import abc as collections_abc
|
||||
except ImportError: # Python 2.7
|
||||
import collections as collections_abc
|
||||
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
time_isoformat = datetime_module.time.isoformat
|
||||
else:
|
||||
def time_isoformat(time, timespec='microseconds'):
|
||||
assert isinstance(time, datetime_module.time)
|
||||
if timespec != 'microseconds':
|
||||
raise NotImplementedError
|
||||
result = '{:02d}:{:02d}:{:02d}.{:06d}'.format(
|
||||
time.hour, time.minute, time.second, time.microsecond
|
||||
)
|
||||
assert len(result) == 15
|
||||
return result
|
||||
|
||||
|
||||
def timedelta_format(timedelta):
|
||||
time = (datetime_module.datetime.min + timedelta).time()
|
||||
return time_isoformat(time, timespec='microseconds')
|
||||
|
||||
def timedelta_parse(s):
|
||||
hours, minutes, seconds, microseconds = map(
|
||||
int,
|
||||
s.replace('.', ':').split(':')
|
||||
)
|
||||
return datetime_module.timedelta(hours=hours, minutes=minutes,
|
||||
seconds=seconds,
|
||||
microseconds=microseconds)
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
# Copyright 2019 Ram Rachum and collaborators.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import opcode
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import collections
|
||||
import datetime as datetime_module
|
||||
import itertools
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from .variables import CommonVariable, Exploding, BaseVariable
|
||||
from . import utils, pycompat
|
||||
if pycompat.PY2:
|
||||
from io import open
|
||||
|
||||
|
||||
ipython_filename_pattern = re.compile('^<ipython-input-([0-9]+)-.*>$')
|
||||
|
||||
|
||||
def get_local_reprs(frame, watch=(), custom_repr=(), max_length=None, normalize=False):
|
||||
code = frame.f_code
|
||||
vars_order = (code.co_varnames + code.co_cellvars + code.co_freevars +
|
||||
tuple(frame.f_locals.keys()))
|
||||
|
||||
result_items = [(key, utils.get_shortish_repr(value, custom_repr,
|
||||
max_length, normalize))
|
||||
for key, value in frame.f_locals.items()]
|
||||
result_items.sort(key=lambda key_value: vars_order.index(key_value[0]))
|
||||
result = collections.OrderedDict(result_items)
|
||||
|
||||
for variable in watch:
|
||||
result.update(sorted(variable.items(frame, normalize)))
|
||||
return result
|
||||
|
||||
|
||||
class UnavailableSource(object):
|
||||
def __getitem__(self, i):
|
||||
return u'SOURCE IS UNAVAILABLE'
|
||||
|
||||
|
||||
source_and_path_cache = {}
|
||||
|
||||
|
||||
def get_path_and_source_from_frame(frame):
|
||||
globs = frame.f_globals or {}
|
||||
module_name = globs.get('__name__')
|
||||
file_name = frame.f_code.co_filename
|
||||
cache_key = (module_name, file_name)
|
||||
try:
|
||||
return source_and_path_cache[cache_key]
|
||||
except KeyError:
|
||||
pass
|
||||
loader = globs.get('__loader__')
|
||||
|
||||
source = None
|
||||
if hasattr(loader, 'get_source'):
|
||||
try:
|
||||
source = loader.get_source(module_name)
|
||||
except ImportError:
|
||||
pass
|
||||
if source is not None:
|
||||
source = source.splitlines()
|
||||
if source is None:
|
||||
ipython_filename_match = ipython_filename_pattern.match(file_name)
|
||||
if ipython_filename_match:
|
||||
entry_number = int(ipython_filename_match.group(1))
|
||||
try:
|
||||
import IPython
|
||||
ipython_shell = IPython.get_ipython()
|
||||
((_, _, source_chunk),) = ipython_shell.history_manager. \
|
||||
get_range(0, entry_number, entry_number + 1)
|
||||
source = source_chunk.splitlines()
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
with open(file_name, 'rb') as fp:
|
||||
source = fp.read().splitlines()
|
||||
except utils.file_reading_errors:
|
||||
pass
|
||||
if not source:
|
||||
# We used to check `if source is None` but I found a rare bug where it
|
||||
# was empty, but not `None`, so now we check `if not source`.
|
||||
source = UnavailableSource()
|
||||
|
||||
# If we just read the source from a file, or if the loader did not
|
||||
# apply tokenize.detect_encoding to decode the source into a
|
||||
# string, then we should do that ourselves.
|
||||
if isinstance(source[0], bytes):
|
||||
encoding = 'utf-8'
|
||||
for line in source[:2]:
|
||||
# File coding may be specified. Match pattern from PEP-263
|
||||
# (https://www.python.org/dev/peps/pep-0263/)
|
||||
match = re.search(br'coding[:=]\s*([-\w.]+)', line)
|
||||
if match:
|
||||
encoding = match.group(1).decode('ascii')
|
||||
break
|
||||
source = [pycompat.text_type(sline, encoding, 'replace') for sline in
|
||||
source]
|
||||
|
||||
result = (file_name, source)
|
||||
source_and_path_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
|
||||
def get_write_function(output, overwrite):
|
||||
is_path = isinstance(output, (pycompat.PathLike, str))
|
||||
if overwrite and not is_path:
|
||||
raise Exception('`overwrite=True` can only be used when writing '
|
||||
'content to file.')
|
||||
if output is None:
|
||||
def write(s):
|
||||
stderr = sys.stderr
|
||||
try:
|
||||
stderr.write(s)
|
||||
except UnicodeEncodeError:
|
||||
# God damn Python 2
|
||||
stderr.write(utils.shitcode(s))
|
||||
elif is_path:
|
||||
return FileWriter(output, overwrite).write
|
||||
elif callable(output):
|
||||
write = output
|
||||
else:
|
||||
assert isinstance(output, utils.WritableStream)
|
||||
|
||||
def write(s):
|
||||
output.write(s)
|
||||
return write
|
||||
|
||||
|
||||
class FileWriter(object):
|
||||
def __init__(self, path, overwrite):
|
||||
self.path = pycompat.text_type(path)
|
||||
self.overwrite = overwrite
|
||||
|
||||
def write(self, s):
|
||||
with open(self.path, 'w' if self.overwrite else 'a',
|
||||
encoding='utf-8') as output_file:
|
||||
output_file.write(s)
|
||||
self.overwrite = False
|
||||
|
||||
|
||||
thread_global = threading.local()
|
||||
DISABLED = bool(os.getenv('PYSNOOPER_DISABLED', ''))
|
||||
|
||||
class Tracer:
|
||||
'''
|
||||
Snoop on the function, writing everything it's doing to stderr.
|
||||
|
||||
This is useful for debugging.
|
||||
|
||||
When you decorate a function with `@pysnooper.snoop()`
|
||||
or wrap a block of code in `with pysnooper.snoop():`, you'll get a log of
|
||||
every line that ran in the function and a play-by-play of every local
|
||||
variable that changed.
|
||||
|
||||
If stderr is not easily accessible for you, you can redirect the output to
|
||||
a file::
|
||||
|
||||
@pysnooper.snoop('/my/log/file.log')
|
||||
|
||||
See values of some expressions that aren't local variables::
|
||||
|
||||
@pysnooper.snoop(watch=('foo.bar', 'self.x["whatever"]'))
|
||||
|
||||
Expand values to see all their attributes or items of lists/dictionaries:
|
||||
|
||||
@pysnooper.snoop(watch_explode=('foo', 'self'))
|
||||
|
||||
(see Advanced Usage in the README for more control)
|
||||
|
||||
Show snoop lines for functions that your function calls::
|
||||
|
||||
@pysnooper.snoop(depth=2)
|
||||
|
||||
Start all snoop lines with a prefix, to grep for them easily::
|
||||
|
||||
@pysnooper.snoop(prefix='ZZZ ')
|
||||
|
||||
On multi-threaded apps identify which thread are snooped in output::
|
||||
|
||||
@pysnooper.snoop(thread_info=True)
|
||||
|
||||
Customize how values are represented as strings::
|
||||
|
||||
@pysnooper.snoop(custom_repr=((type1, custom_repr_func1),
|
||||
(condition2, custom_repr_func2), ...))
|
||||
|
||||
Variables and exceptions get truncated to 100 characters by default. You
|
||||
can customize that:
|
||||
|
||||
@pysnooper.snoop(max_variable_length=200)
|
||||
|
||||
You can also use `max_variable_length=None` to never truncate them.
|
||||
|
||||
Show timestamps relative to start time rather than wall time::
|
||||
|
||||
@pysnooper.snoop(relative_time=True)
|
||||
|
||||
'''
|
||||
def __init__(self, output=None, watch=(), watch_explode=(), depth=1,
|
||||
prefix='', overwrite=False, thread_info=False, custom_repr=(),
|
||||
max_variable_length=100, normalize=False, relative_time=False):
|
||||
self._write = get_write_function(output, overwrite)
|
||||
|
||||
self.watch = [
|
||||
v if isinstance(v, BaseVariable) else CommonVariable(v)
|
||||
for v in utils.ensure_tuple(watch)
|
||||
] + [
|
||||
v if isinstance(v, BaseVariable) else Exploding(v)
|
||||
for v in utils.ensure_tuple(watch_explode)
|
||||
]
|
||||
self.frame_to_local_reprs = {}
|
||||
self.start_times = {}
|
||||
self.depth = depth
|
||||
self.prefix = prefix
|
||||
self.thread_info = thread_info
|
||||
self.thread_info_padding = 0
|
||||
assert self.depth >= 1
|
||||
self.target_codes = set()
|
||||
self.target_frames = set()
|
||||
self.thread_local = threading.local()
|
||||
if len(custom_repr) == 2 and not all(isinstance(x,
|
||||
pycompat.collections_abc.Iterable) for x in custom_repr):
|
||||
custom_repr = (custom_repr,)
|
||||
self.custom_repr = custom_repr
|
||||
self.last_source_path = None
|
||||
self.max_variable_length = max_variable_length
|
||||
self.normalize = normalize
|
||||
self.relative_time = relative_time
|
||||
|
||||
def __call__(self, function_or_class):
|
||||
if DISABLED:
|
||||
return function_or_class
|
||||
|
||||
if inspect.isclass(function_or_class):
|
||||
return self._wrap_class(function_or_class)
|
||||
else:
|
||||
return self._wrap_function(function_or_class)
|
||||
|
||||
def _wrap_class(self, cls):
|
||||
for attr_name, attr in cls.__dict__.items():
|
||||
# Coroutines are functions, but snooping them is not supported
|
||||
# at the moment
|
||||
if pycompat.iscoroutinefunction(attr):
|
||||
continue
|
||||
|
||||
if inspect.isfunction(attr):
|
||||
setattr(cls, attr_name, self._wrap_function(attr))
|
||||
return cls
|
||||
|
||||
def _wrap_function(self, function):
|
||||
self.target_codes.add(function.__code__)
|
||||
|
||||
@functools.wraps(function)
|
||||
def simple_wrapper(*args, **kwargs):
|
||||
with self:
|
||||
return function(*args, **kwargs)
|
||||
|
||||
@functools.wraps(function)
|
||||
def generator_wrapper(*args, **kwargs):
|
||||
gen = function(*args, **kwargs)
|
||||
method, incoming = gen.send, None
|
||||
while True:
|
||||
with self:
|
||||
try:
|
||||
outgoing = method(incoming)
|
||||
except StopIteration:
|
||||
return
|
||||
try:
|
||||
method, incoming = gen.send, (yield outgoing)
|
||||
except Exception as e:
|
||||
method, incoming = gen.throw, e
|
||||
|
||||
if pycompat.iscoroutinefunction(function):
|
||||
raise NotImplementedError
|
||||
if pycompat.isasyncgenfunction(function):
|
||||
raise NotImplementedError
|
||||
elif inspect.isgeneratorfunction(function):
|
||||
return generator_wrapper
|
||||
else:
|
||||
return simple_wrapper
|
||||
|
||||
def write(self, s):
|
||||
s = u'{self.prefix}{s}\n'.format(**locals())
|
||||
self._write(s)
|
||||
|
||||
def __enter__(self):
|
||||
if DISABLED:
|
||||
return
|
||||
calling_frame = inspect.currentframe().f_back
|
||||
if not self._is_internal_frame(calling_frame):
|
||||
calling_frame.f_trace = self.trace
|
||||
self.target_frames.add(calling_frame)
|
||||
|
||||
stack = self.thread_local.__dict__.setdefault(
|
||||
'original_trace_functions', []
|
||||
)
|
||||
stack.append(sys.gettrace())
|
||||
self.start_times[calling_frame] = datetime_module.datetime.now()
|
||||
sys.settrace(self.trace)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
if DISABLED:
|
||||
return
|
||||
stack = self.thread_local.original_trace_functions
|
||||
sys.settrace(stack.pop())
|
||||
calling_frame = inspect.currentframe().f_back
|
||||
self.target_frames.discard(calling_frame)
|
||||
self.frame_to_local_reprs.pop(calling_frame, None)
|
||||
|
||||
### Writing elapsed time: #############################################
|
||||
# #
|
||||
start_time = self.start_times.pop(calling_frame)
|
||||
duration = datetime_module.datetime.now() - start_time
|
||||
elapsed_time_string = pycompat.timedelta_format(duration)
|
||||
indent = ' ' * 4 * (thread_global.depth + 1)
|
||||
self.write(
|
||||
'{indent}Elapsed time: {elapsed_time_string}'.format(**locals())
|
||||
)
|
||||
# #
|
||||
### Finished writing elapsed time. ####################################
|
||||
|
||||
def _is_internal_frame(self, frame):
|
||||
return frame.f_code.co_filename == Tracer.__enter__.__code__.co_filename
|
||||
|
||||
def set_thread_info_padding(self, thread_info):
|
||||
current_thread_len = len(thread_info)
|
||||
self.thread_info_padding = max(self.thread_info_padding,
|
||||
current_thread_len)
|
||||
return thread_info.ljust(self.thread_info_padding)
|
||||
|
||||
def trace(self, frame, event, arg):
|
||||
|
||||
### Checking whether we should trace this line: #######################
|
||||
# #
|
||||
# We should trace this line either if it's in the decorated function,
|
||||
# or the user asked to go a few levels deeper and we're within that
|
||||
# number of levels deeper.
|
||||
|
||||
if not (frame.f_code in self.target_codes or frame in self.target_frames):
|
||||
if self.depth == 1:
|
||||
# We did the most common and quickest check above, because the
|
||||
# trace function runs so incredibly often, therefore it's
|
||||
# crucial to hyper-optimize it for the common case.
|
||||
return None
|
||||
elif self._is_internal_frame(frame):
|
||||
return None
|
||||
else:
|
||||
_frame_candidate = frame
|
||||
for i in range(1, self.depth):
|
||||
_frame_candidate = _frame_candidate.f_back
|
||||
if _frame_candidate is None:
|
||||
return None
|
||||
elif _frame_candidate.f_code in self.target_codes or _frame_candidate in self.target_frames:
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
thread_global.__dict__.setdefault('depth', -1)
|
||||
if event == 'call':
|
||||
thread_global.depth += 1
|
||||
indent = ' ' * 4 * thread_global.depth
|
||||
|
||||
# #
|
||||
### Finished checking whether we should trace this line. ##############
|
||||
|
||||
### Making timestamp: #################################################
|
||||
# #
|
||||
if self.normalize:
|
||||
timestamp = ' ' * 15
|
||||
elif self.relative_time:
|
||||
try:
|
||||
start_time = self.start_times[frame]
|
||||
except KeyError:
|
||||
start_time = self.start_times[frame] = \
|
||||
datetime_module.datetime.now()
|
||||
duration = datetime_module.datetime.now() - start_time
|
||||
timestamp = pycompat.timedelta_format(duration)
|
||||
else:
|
||||
timestamp = pycompat.time_isoformat(
|
||||
datetime_module.datetime.now().time(),
|
||||
timespec='microseconds'
|
||||
)
|
||||
# #
|
||||
### Finished making timestamp. ########################################
|
||||
|
||||
line_no = frame.f_lineno
|
||||
source_path, source = get_path_and_source_from_frame(frame)
|
||||
source_path = source_path if not self.normalize else os.path.basename(source_path)
|
||||
if self.last_source_path != source_path:
|
||||
self.write(u'{indent}Source path:... {source_path}'.
|
||||
format(**locals()))
|
||||
self.last_source_path = source_path
|
||||
source_line = source[line_no - 1]
|
||||
thread_info = ""
|
||||
if self.thread_info:
|
||||
if self.normalize:
|
||||
raise NotImplementedError("normalize is not supported with "
|
||||
"thread_info")
|
||||
current_thread = threading.current_thread()
|
||||
thread_info = "{ident}-{name} ".format(
|
||||
ident=current_thread.ident, name=current_thread.getName())
|
||||
thread_info = self.set_thread_info_padding(thread_info)
|
||||
|
||||
### Reporting newish and modified variables: ##########################
|
||||
# #
|
||||
old_local_reprs = self.frame_to_local_reprs.get(frame, {})
|
||||
self.frame_to_local_reprs[frame] = local_reprs = \
|
||||
get_local_reprs(frame,
|
||||
watch=self.watch, custom_repr=self.custom_repr,
|
||||
max_length=self.max_variable_length,
|
||||
normalize=self.normalize,
|
||||
)
|
||||
|
||||
newish_string = ('Starting var:.. ' if event == 'call' else
|
||||
'New var:....... ')
|
||||
|
||||
for name, value_repr in local_reprs.items():
|
||||
if name not in old_local_reprs:
|
||||
self.write('{indent}{newish_string}{name} = {value_repr}'.format(
|
||||
**locals()))
|
||||
elif old_local_reprs[name] != value_repr:
|
||||
self.write('{indent}Modified var:.. {name} = {value_repr}'.format(
|
||||
**locals()))
|
||||
|
||||
# #
|
||||
### Finished newish and modified variables. ###########################
|
||||
|
||||
|
||||
### Dealing with misplaced function definition: #######################
|
||||
# #
|
||||
if event == 'call' and source_line.lstrip().startswith('@'):
|
||||
# If a function decorator is found, skip lines until an actual
|
||||
# function definition is found.
|
||||
for candidate_line_no in itertools.count(line_no):
|
||||
try:
|
||||
candidate_source_line = source[candidate_line_no - 1]
|
||||
except IndexError:
|
||||
# End of source file reached without finding a function
|
||||
# definition. Fall back to original source line.
|
||||
break
|
||||
|
||||
if candidate_source_line.lstrip().startswith('def'):
|
||||
# Found the def line!
|
||||
line_no = candidate_line_no
|
||||
source_line = candidate_source_line
|
||||
break
|
||||
# #
|
||||
### Finished dealing with misplaced function definition. ##############
|
||||
|
||||
# If a call ends due to an exception, we still get a 'return' event
|
||||
# with arg = None. This seems to be the only way to tell the difference
|
||||
# https://stackoverflow.com/a/12800909/2482744
|
||||
code_byte = frame.f_code.co_code[frame.f_lasti]
|
||||
if not isinstance(code_byte, int):
|
||||
code_byte = ord(code_byte)
|
||||
ended_by_exception = (
|
||||
event == 'return'
|
||||
and arg is None
|
||||
and (opcode.opname[code_byte]
|
||||
not in ('RETURN_VALUE', 'YIELD_VALUE'))
|
||||
)
|
||||
|
||||
if ended_by_exception:
|
||||
self.write('{indent}Call ended by exception'.
|
||||
format(**locals()))
|
||||
else:
|
||||
self.write(u'{indent}{timestamp} {thread_info}{event:9} '
|
||||
u'{line_no:4} {source_line}'.format(**locals()))
|
||||
|
||||
if event == 'return':
|
||||
self.frame_to_local_reprs.pop(frame, None)
|
||||
self.start_times.pop(frame, None)
|
||||
thread_global.depth -= 1
|
||||
|
||||
if not ended_by_exception:
|
||||
return_value_repr = utils.get_shortish_repr(arg,
|
||||
custom_repr=self.custom_repr,
|
||||
max_length=self.max_variable_length,
|
||||
normalize=self.normalize,
|
||||
)
|
||||
self.write('{indent}Return value:.. {return_value_repr}'.
|
||||
format(**locals()))
|
||||
|
||||
if event == 'exception':
|
||||
exception = '\n'.join(traceback.format_exception_only(*arg[:2])).strip()
|
||||
if self.max_variable_length:
|
||||
exception = utils.truncate(exception, self.max_variable_length)
|
||||
self.write('{indent}{exception}'.
|
||||
format(**locals()))
|
||||
|
||||
return self.trace
|
|
@ -0,0 +1,98 @@
|
|||
# Copyright 2019 Ram Rachum and collaborators.
|
||||
# This program is distributed under the MIT license.
|
||||
|
||||
import abc
|
||||
import re
|
||||
|
||||
import sys
|
||||
from .pycompat import ABC, string_types, collections_abc
|
||||
|
||||
def _check_methods(C, *methods):
|
||||
mro = C.__mro__
|
||||
for method in methods:
|
||||
for B in mro:
|
||||
if method in B.__dict__:
|
||||
if B.__dict__[method] is None:
|
||||
return NotImplemented
|
||||
break
|
||||
else:
|
||||
return NotImplemented
|
||||
return True
|
||||
|
||||
|
||||
class WritableStream(ABC):
|
||||
@abc.abstractmethod
|
||||
def write(self, s):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is WritableStream:
|
||||
return _check_methods(C, 'write')
|
||||
return NotImplemented
|
||||
|
||||
|
||||
|
||||
file_reading_errors = (
|
||||
IOError,
|
||||
OSError,
|
||||
ValueError # IronPython weirdness.
|
||||
)
|
||||
|
||||
|
||||
|
||||
def shitcode(s):
|
||||
return ''.join(
|
||||
(c if (0 < ord(c) < 256) else '?') for c in s
|
||||
)
|
||||
|
||||
|
||||
def get_repr_function(item, custom_repr):
|
||||
for condition, action in custom_repr:
|
||||
if isinstance(condition, type):
|
||||
condition = lambda x, y=condition: isinstance(x, y)
|
||||
if condition(item):
|
||||
return action
|
||||
return repr
|
||||
|
||||
|
||||
DEFAULT_REPR_RE = re.compile(r' at 0x[a-f0-9A-F]{4,}')
|
||||
|
||||
|
||||
def normalize_repr(item_repr):
|
||||
"""Remove memory address (0x...) from a default python repr"""
|
||||
return DEFAULT_REPR_RE.sub('', item_repr)
|
||||
|
||||
|
||||
def get_shortish_repr(item, custom_repr=(), max_length=None, normalize=False):
|
||||
repr_function = get_repr_function(item, custom_repr)
|
||||
try:
|
||||
r = repr_function(item)
|
||||
except Exception:
|
||||
r = 'REPR FAILED'
|
||||
r = r.replace('\r', '').replace('\n', '')
|
||||
if normalize:
|
||||
r = normalize_repr(r)
|
||||
if max_length:
|
||||
r = truncate(r, max_length)
|
||||
return r
|
||||
|
||||
|
||||
def truncate(string, max_length):
|
||||
if (max_length is None) or (len(string) <= max_length):
|
||||
return string
|
||||
else:
|
||||
left = (max_length - 3) // 2
|
||||
right = max_length - 3 - left
|
||||
return u'{}...{}'.format(string[:left], string[-right:])
|
||||
|
||||
|
||||
def ensure_tuple(x):
|
||||
if isinstance(x, collections_abc.Iterable) and \
|
||||
not isinstance(x, string_types):
|
||||
return tuple(x)
|
||||
else:
|
||||
return (x,)
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import itertools
|
||||
import abc
|
||||
try:
|
||||
from collections.abc import Mapping, Sequence
|
||||
except ImportError:
|
||||
from collections import Mapping, Sequence
|
||||
from copy import deepcopy
|
||||
|
||||
from . import utils
|
||||
from . import pycompat
|
||||
|
||||
|
||||
def needs_parentheses(source):
|
||||
def code(s):
|
||||
return compile(s, '<variable>', 'eval').co_code
|
||||
|
||||
return code('{}.x'.format(source)) != code('({}).x'.format(source))
|
||||
|
||||
|
||||
class BaseVariable(pycompat.ABC):
|
||||
def __init__(self, source, exclude=()):
|
||||
self.source = source
|
||||
self.exclude = utils.ensure_tuple(exclude)
|
||||
self.code = compile(source, '<variable>', 'eval')
|
||||
if needs_parentheses(source):
|
||||
self.unambiguous_source = '({})'.format(source)
|
||||
else:
|
||||
self.unambiguous_source = source
|
||||
|
||||
def items(self, frame, normalize=False):
|
||||
try:
|
||||
main_value = eval(self.code, frame.f_globals or {}, frame.f_locals)
|
||||
except Exception:
|
||||
return ()
|
||||
return self._items(main_value, normalize)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _items(self, key, normalize=False):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def _fingerprint(self):
|
||||
return (type(self), self.source, self.exclude)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._fingerprint)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, BaseVariable) and
|
||||
self._fingerprint == other._fingerprint)
|
||||
|
||||
|
||||
class CommonVariable(BaseVariable):
|
||||
def _items(self, main_value, normalize=False):
|
||||
result = [(self.source, utils.get_shortish_repr(main_value, normalize=normalize))]
|
||||
for key in self._safe_keys(main_value):
|
||||
try:
|
||||
if key in self.exclude:
|
||||
continue
|
||||
value = self._get_value(main_value, key)
|
||||
except Exception:
|
||||
continue
|
||||
result.append((
|
||||
'{}{}'.format(self.unambiguous_source, self._format_key(key)),
|
||||
utils.get_shortish_repr(value)
|
||||
))
|
||||
return result
|
||||
|
||||
def _safe_keys(self, main_value):
|
||||
try:
|
||||
for key in self._keys(main_value):
|
||||
yield key
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _keys(self, main_value):
|
||||
return ()
|
||||
|
||||
def _format_key(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_value(self, main_value, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Attrs(CommonVariable):
|
||||
def _keys(self, main_value):
|
||||
return itertools.chain(
|
||||
getattr(main_value, '__dict__', ()),
|
||||
getattr(main_value, '__slots__', ())
|
||||
)
|
||||
|
||||
def _format_key(self, key):
|
||||
return '.' + key
|
||||
|
||||
def _get_value(self, main_value, key):
|
||||
return getattr(main_value, key)
|
||||
|
||||
|
||||
class Keys(CommonVariable):
|
||||
def _keys(self, main_value):
|
||||
return main_value.keys()
|
||||
|
||||
def _format_key(self, key):
|
||||
return '[{}]'.format(utils.get_shortish_repr(key))
|
||||
|
||||
def _get_value(self, main_value, key):
|
||||
return main_value[key]
|
||||
|
||||
|
||||
class Indices(Keys):
|
||||
_slice = slice(None)
|
||||
|
||||
def _keys(self, main_value):
|
||||
return range(len(main_value))[self._slice]
|
||||
|
||||
def __getitem__(self, item):
|
||||
assert isinstance(item, slice)
|
||||
result = deepcopy(self)
|
||||
result._slice = item
|
||||
return result
|
||||
|
||||
|
||||
class Exploding(BaseVariable):
|
||||
def _items(self, main_value, normalize=False):
|
||||
if isinstance(main_value, Mapping):
|
||||
cls = Keys
|
||||
elif isinstance(main_value, Sequence):
|
||||
cls = Indices
|
||||
else:
|
||||
cls = Attrs
|
||||
|
||||
return cls(self.source, self.exclude)._items(main_value, normalize)
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
exec 2>&1
|
||||
exec multilog t s25000 n4 /var/log/dbus-fzsonick-48tl.TTY
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
exec 2>&1
|
||||
|
||||
exec softlimit -d 100000000 -s 1000000 -a 100000000 /opt/innovenergy/dbus-fzsonick-48tl/start.sh TTY
|
|
@ -0,0 +1,214 @@
|
|||
# coding=utf-8
|
||||
|
||||
import config as cfg
|
||||
from convert import mean, read_float, read_led_state, read_bool, count_bits, comma_separated
|
||||
from data import BatterySignal, Battery, LedColor, ServiceSignal, BatteryStatus, LedState
|
||||
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import List, Iterable
|
||||
|
||||
|
||||
def init_service_signals(batteries):
|
||||
# type: (List[Battery]) -> Iterable[ServiceSignal]
|
||||
|
||||
n_batteries = len(batteries)
|
||||
product_name = cfg.PRODUCT_NAME + ' x' + str(n_batteries)
|
||||
|
||||
return [
|
||||
ServiceSignal('/NbOfBatteries', n_batteries), # TODO: nb of operational batteries
|
||||
ServiceSignal('/Mgmt/ProcessName', __file__),
|
||||
ServiceSignal('/Mgmt/ProcessVersion', cfg.SOFTWARE_VERSION),
|
||||
ServiceSignal('/Mgmt/Connection', cfg.CONNECTION),
|
||||
ServiceSignal('/DeviceInstance', cfg.DEVICE_INSTANCE),
|
||||
ServiceSignal('/ProductName', product_name),
|
||||
ServiceSignal('/ProductId', cfg.PRODUCT_ID),
|
||||
ServiceSignal('/Connected', 1)
|
||||
]
|
||||
|
||||
|
||||
def init_battery_signals():
|
||||
# type: () -> Iterable[BatterySignal]
|
||||
|
||||
read_voltage = read_float(register=999, scale_factor=0.01, offset=0)
|
||||
read_current = read_float(register=1000, scale_factor=0.01, offset=-10000)
|
||||
|
||||
read_led_amber = read_led_state(register=1004, led=LedColor.amber)
|
||||
read_led_green = read_led_state(register=1004, led=LedColor.green)
|
||||
read_led_blue = read_led_state(register=1004, led=LedColor.blue)
|
||||
read_led_red = read_led_state(register=1004, led=LedColor.red)
|
||||
|
||||
def read_power(status):
|
||||
# type: (BatteryStatus) -> int
|
||||
return int(read_current(status) * read_voltage(status))
|
||||
|
||||
def calc_power_limit_imposed_by_voltage_limit(v, i, v_limit, r_int):
|
||||
# type: (float, float, float, float) -> float
|
||||
|
||||
dv = v_limit - v
|
||||
di = dv / r_int
|
||||
p_limit = v_limit * (i + di)
|
||||
|
||||
return p_limit
|
||||
|
||||
def calc_power_limit_imposed_by_current_limit(v, i, i_limit, r_int):
|
||||
# type: (float, float, float, float) -> float
|
||||
|
||||
di = i_limit - i
|
||||
dv = di * r_int
|
||||
p_limit = i_limit * (v + dv)
|
||||
|
||||
return p_limit
|
||||
|
||||
def calc_max_charge_power(bs):
|
||||
# type: (BatteryStatus) -> int
|
||||
|
||||
b = bs.battery
|
||||
v = read_voltage(bs)
|
||||
i = read_current(bs)
|
||||
|
||||
p_limits = [
|
||||
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_max, b.r_int_min),
|
||||
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_max, b.r_int_max),
|
||||
calc_power_limit_imposed_by_current_limit(v, i, b.i_max, b.r_int_min),
|
||||
calc_power_limit_imposed_by_current_limit(v, i, b.i_max, b.r_int_max),
|
||||
]
|
||||
|
||||
p_limit = min(p_limits) # p_limit is normally positive here (signed)
|
||||
p_limit = max(p_limit, 0) # charge power must not become negative
|
||||
|
||||
return int(p_limit)
|
||||
|
||||
def calc_max_discharge_power(bs):
|
||||
# type: (BatteryStatus) -> float
|
||||
|
||||
b = bs.battery
|
||||
v = read_voltage(bs)
|
||||
i = read_current(bs)
|
||||
|
||||
p_limits = [
|
||||
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_min, b.r_int_min),
|
||||
calc_power_limit_imposed_by_voltage_limit(v, i, b.v_min, b.r_int_max),
|
||||
calc_power_limit_imposed_by_current_limit(v, i, -b.i_max, b.r_int_min),
|
||||
calc_power_limit_imposed_by_current_limit(v, i, -b.i_max, b.r_int_max),
|
||||
]
|
||||
|
||||
p_limit = max(p_limits) # p_limit is normally negative here (signed)
|
||||
p_limit = min(p_limit, 0) # discharge power must not become positive
|
||||
|
||||
return int(-p_limit) # make unsigned!
|
||||
|
||||
def read_battery_cold(status):
|
||||
return \
|
||||
read_led_green(status) >= LedState.blinking_slow and \
|
||||
read_led_blue(status) >= LedState.blinking_slow
|
||||
|
||||
def read_soc(status):
|
||||
soc = read_float(register=1053, scale_factor=0.1, offset=0)(status)
|
||||
|
||||
# if the SOC is 100 but EOC is not yet reached, report 99.9 instead of 100
|
||||
if soc > 99.9 and not read_eoc_reached(status):
|
||||
return 99.9
|
||||
if soc >= 99.9 and read_eoc_reached(status):
|
||||
return 100
|
||||
|
||||
return soc
|
||||
|
||||
def read_eoc_reached(status):
|
||||
return \
|
||||
read_led_green(status) == LedState.on and \
|
||||
read_led_amber(status) == LedState.off and \
|
||||
read_led_blue(status) == LedState.off
|
||||
|
||||
return [
|
||||
BatterySignal('/Dc/0/Voltage', mean, get_value=read_voltage, unit='V'),
|
||||
BatterySignal('/Dc/0/Current', sum, get_value=read_current, unit='A'),
|
||||
BatterySignal('/Dc/0/Power', sum, get_value=read_power, unit='W'),
|
||||
|
||||
BatterySignal('/BussVoltage', mean, read_float(register=1001, scale_factor=0.01, offset=0), unit='V'),
|
||||
BatterySignal('/Soc', mean, read_soc, unit='%'),
|
||||
BatterySignal('/Dc/0/Temperature', mean, read_float(register=1003, scale_factor=0.1, offset=-400), unit='C'),
|
||||
|
||||
BatterySignal('/NumberOfWarningFlags', sum, count_bits(base_register=1005, nb_of_registers=3, nb_of_bits=47)),
|
||||
BatterySignal('/WarningFlags/TaM1', any, read_bool(base_register=1005, bit=1)),
|
||||
BatterySignal('/WarningFlags/TbM1', any, read_bool(base_register=1005, bit=4)),
|
||||
BatterySignal('/WarningFlags/VBm1', any, read_bool(base_register=1005, bit=6)),
|
||||
BatterySignal('/WarningFlags/VBM1', any, read_bool(base_register=1005, bit=8)),
|
||||
BatterySignal('/WarningFlags/IDM1', any, read_bool(base_register=1005, bit=10)),
|
||||
BatterySignal('/WarningFlags/vsM1', any, read_bool(base_register=1005, bit=24)),
|
||||
BatterySignal('/WarningFlags/iCM1', any, read_bool(base_register=1005, bit=26)),
|
||||
BatterySignal('/WarningFlags/iDM1', any, read_bool(base_register=1005, bit=28)),
|
||||
BatterySignal('/WarningFlags/MID1', any, read_bool(base_register=1005, bit=30)),
|
||||
BatterySignal('/WarningFlags/BLPW', any, read_bool(base_register=1005, bit=32)),
|
||||
BatterySignal('/WarningFlags/Ah_W', any, read_bool(base_register=1005, bit=35)),
|
||||
BatterySignal('/WarningFlags/MPMM', any, read_bool(base_register=1005, bit=38)),
|
||||
BatterySignal('/WarningFlags/TCMM', any, read_bool(base_register=1005, bit=39)),
|
||||
BatterySignal('/WarningFlags/TCdi', any, read_bool(base_register=1005, bit=40)),
|
||||
BatterySignal('/WarningFlags/WMTO', any, read_bool(base_register=1005, bit=41)),
|
||||
BatterySignal('/WarningFlags/bit44', any, read_bool(base_register=1005, bit=44)),
|
||||
BatterySignal('/WarningFlags/CELL1', any, read_bool(base_register=1005, bit=46)),
|
||||
BatterySignal('/WarningFlags/bit47WarningDummy', any, read_bool(base_register=1005, bit=47)),
|
||||
|
||||
BatterySignal('/NumberOfAlarmFlags', sum, count_bits(base_register=1009, nb_of_registers=3, nb_of_bits=47)),
|
||||
BatterySignal('/AlarmFlags/Tam', any, read_bool(base_register=1009, bit=0)),
|
||||
BatterySignal('/AlarmFlags/TaM2', any, read_bool(base_register=1009, bit=2)),
|
||||
BatterySignal('/AlarmFlags/Tbm', any, read_bool(base_register=1009, bit=3)),
|
||||
BatterySignal('/AlarmFlags/TbM2', any, read_bool(base_register=1009, bit=5)),
|
||||
BatterySignal('/AlarmFlags/VBm2', any, read_bool(base_register=1009, bit=7)),
|
||||
BatterySignal('/AlarmFlags/IDM2', any, read_bool(base_register=1009, bit=11)),
|
||||
BatterySignal('/AlarmFlags/ISOB', any, read_bool(base_register=1009, bit=12)),
|
||||
BatterySignal('/AlarmFlags/MSWE', any, read_bool(base_register=1009, bit=13)),
|
||||
BatterySignal('/AlarmFlags/FUSE', any, read_bool(base_register=1009, bit=14)),
|
||||
BatterySignal('/AlarmFlags/HTRE', any, read_bool(base_register=1009, bit=15)),
|
||||
BatterySignal('/AlarmFlags/TCPE', any, read_bool(base_register=1009, bit=16)),
|
||||
BatterySignal('/AlarmFlags/STRE', any, read_bool(base_register=1009, bit=17)),
|
||||
BatterySignal('/AlarmFlags/CME', any, read_bool(base_register=1009, bit=18)),
|
||||
BatterySignal('/AlarmFlags/HWFL', any, read_bool(base_register=1009, bit=19)),
|
||||
BatterySignal('/AlarmFlags/HWEM', any, read_bool(base_register=1009, bit=20)),
|
||||
BatterySignal('/AlarmFlags/ThM', any, read_bool(base_register=1009, bit=21)),
|
||||
BatterySignal('/AlarmFlags/vsm1', any, read_bool(base_register=1009, bit=22)),
|
||||
BatterySignal('/AlarmFlags/vsm2', any, read_bool(base_register=1009, bit=23)),
|
||||
BatterySignal('/AlarmFlags/vsM2', any, read_bool(base_register=1009, bit=25)),
|
||||
BatterySignal('/AlarmFlags/iCM2', any, read_bool(base_register=1009, bit=27)),
|
||||
BatterySignal('/AlarmFlags/iDM2', any, read_bool(base_register=1009, bit=29)),
|
||||
BatterySignal('/AlarmFlags/MID2', any, read_bool(base_register=1009, bit=31)),
|
||||
BatterySignal('/AlarmFlags/CCBF', any, read_bool(base_register=1009, bit=33)),
|
||||
BatterySignal('/AlarmFlags/AhFL', any, read_bool(base_register=1009, bit=34)),
|
||||
BatterySignal('/AlarmFlags/TbCM', any, read_bool(base_register=1009, bit=36)),
|
||||
BatterySignal('/AlarmFlags/BRNF', any, read_bool(base_register=1009, bit=37)),
|
||||
BatterySignal('/AlarmFlags/HTFS', any, read_bool(base_register=1009, bit=42)),
|
||||
BatterySignal('/AlarmFlags/DATA', any, read_bool(base_register=1009, bit=43)),
|
||||
BatterySignal('/AlarmFlags/CELL2', any, read_bool(base_register=1009, bit=45)),
|
||||
BatterySignal('/AlarmFlags/bit47AlarmDummy', any, read_bool(base_register=1009, bit=47)),
|
||||
|
||||
BatterySignal('/LedStatus/Red', max, read_led_red),
|
||||
BatterySignal('/LedStatus/Blue', max, read_led_blue),
|
||||
BatterySignal('/LedStatus/Green', max, read_led_green),
|
||||
BatterySignal('/LedStatus/Amber', max, read_led_amber),
|
||||
|
||||
BatterySignal('/IoStatus/MainSwitchClosed', any, read_bool(base_register=1013, bit=0)),
|
||||
BatterySignal('/IoStatus/AlarmOutActive', any, read_bool(base_register=1013, bit=1)),
|
||||
BatterySignal('/IoStatus/InternalFanActive', any, read_bool(base_register=1013, bit=2)),
|
||||
BatterySignal('/IoStatus/VoltMeasurementAllowed', any, read_bool(base_register=1013, bit=3)),
|
||||
BatterySignal('/IoStatus/AuxRelay', any, read_bool(base_register=1013, bit=4)),
|
||||
BatterySignal('/IoStatus/RemoteState', any, read_bool(base_register=1013, bit=5)),
|
||||
BatterySignal('/IoStatus/HeaterOn', any, read_bool(base_register=1013, bit=6)),
|
||||
BatterySignal('/IoStatus/EocReached', min, read_eoc_reached),
|
||||
BatterySignal('/IoStatus/BatteryCold', any, read_battery_cold),
|
||||
|
||||
# see protocol doc page 7
|
||||
BatterySignal('/Info/MaxDischargeCurrent', sum, lambda bs: bs.battery.i_max, unit='A'),
|
||||
BatterySignal('/Info/MaxChargeCurrent', sum, lambda bs: bs.battery.i_max, unit='A'),
|
||||
BatterySignal('/Info/MaxChargeVoltage', min, lambda bs: bs.battery.v_max, unit='V'),
|
||||
BatterySignal('/Info/MinDischargeVoltage', max, lambda bs: bs.battery.v_min, unit='V'),
|
||||
BatterySignal('/Info/BatteryLowVoltage' , max, lambda bs: bs.battery.v_min-2, unit='V'),
|
||||
BatterySignal('/Info/NumberOfStrings', sum, lambda bs: bs.battery.n_strings),
|
||||
|
||||
BatterySignal('/Info/MaxChargePower', sum, calc_max_charge_power),
|
||||
BatterySignal('/Info/MaxDischargePower', sum, calc_max_discharge_power),
|
||||
|
||||
BatterySignal('/FirmwareVersion', comma_separated, lambda bs: bs.battery.firmware_version),
|
||||
BatterySignal('/HardwareVersion', comma_separated, lambda bs: bs.battery.hardware_version),
|
||||
BatterySignal('/BmsVersion', comma_separated, lambda bs: bs.battery.bms_version)
|
||||
|
||||
]
|
Binary file not shown.
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
. /opt/victronenergy/serial-starter/run-service.sh
|
||||
|
||||
app="/opt/innovenergy/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py"
|
||||
args="$tty"
|
||||
start $args
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,55 @@
|
|||
import serial
|
||||
import logging
|
||||
|
||||
# dbus configuration
|
||||
|
||||
FIRMWARE_VERSION = 1 # value returned by getValue (getText returns string value reported by battery)
|
||||
HARDWARE_VERSION = 1 # value returned by getValue (getText returns string value reported by battery)
|
||||
|
||||
CONNECTION = 'Modbus RTU'
|
||||
PRODUCT_NAME = 'FZS 48TL200'
|
||||
PRODUCT_ID = 0xB012 # assigned by victron
|
||||
DEVICE_INSTANCE = 1
|
||||
SERVICE_NAME_PREFIX = 'com.victronenergy.battery.'
|
||||
|
||||
#s3 configuration
|
||||
S3BUCKET = "13-c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"
|
||||
S3KEY = "EXOcca50b894afa583d8d380dd1"
|
||||
S3SECRET = "7fmdIN1WL8WL9k-20YjLZC5liH2qCwYrGP31Y4dityk"
|
||||
|
||||
# driver configuration
|
||||
|
||||
SOFTWARE_VERSION = '3.0.3'
|
||||
UPDATE_INTERVAL = 2000 # milliseconds
|
||||
#LOG_LEVEL = logging.INFO
|
||||
LOG_LEVEL = logging.DEBUG
|
||||
|
||||
# modbus configuration
|
||||
|
||||
BASE_ADDRESS = 999
|
||||
#NO_OF_REGISTERS = 63
|
||||
NO_OF_REGISTERS = 64
|
||||
MAX_SLAVE_ADDRESS = 10
|
||||
|
||||
|
||||
# RS 485 configuration
|
||||
|
||||
PARITY = serial.PARITY_ODD
|
||||
TIMEOUT = 0.1 # seconds
|
||||
BAUD_RATE = 115200
|
||||
BYTE_SIZE = 8
|
||||
STOP_BITS = 1
|
||||
MODE = 'rtu'
|
||||
|
||||
|
||||
# battery configuration
|
||||
|
||||
MAX_CHARGE_VOLTAGE = 58
|
||||
I_MAX_PER_STRING = 15
|
||||
NUM_OF_STRING_PER_BATTERY = 5
|
||||
AH_PER_STRING = 40
|
||||
V_MAX = 54.2
|
||||
R_STRING_MIN = 0.125
|
||||
R_STRING_MAX = 0.250
|
||||
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
from collections import Iterable
|
||||
from decimal import *
|
||||
|
||||
import config as cfg
|
||||
from data import LedState, BatteryStatus
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def read_bool(register, bit):
|
||||
# type: (int, int) -> Callable[[BatteryStatus], bool]
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> bool
|
||||
value = status.modbus_data[register - cfg.BASE_ADDRESS]
|
||||
return value & (1 << bit) > 0
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_float(register, scale_factor=1.0, offset=0.0, places=2):
|
||||
# type: (int, float, float) -> Callable[[BatteryStatus], float]
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> float
|
||||
value = status.modbus_data[register - cfg.BASE_ADDRESS]
|
||||
|
||||
if value >= 0x8000: # convert to signed int16
|
||||
value -= 0x10000 # fiamm stores their integers signed AND with sign-offset @#%^&!
|
||||
|
||||
result = (value+offset)*scale_factor
|
||||
return round(result,places)
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_hex_string(register, count):
|
||||
# type: (int, int) -> Callable[[BatteryStatus], str]
|
||||
"""
|
||||
reads count consecutive modbus registers from start_address,
|
||||
and returns a hex representation of it:
|
||||
e.g. for count=4: DEAD BEEF DEAD BEEF.
|
||||
"""
|
||||
start = register - cfg.BASE_ADDRESS
|
||||
end = start + count
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> str
|
||||
return ' '.join(['{0:0>4X}'.format(x) for x in status.modbus_data[start:end]])
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_led_state(register, led):
|
||||
# type: (int, int) -> Callable[[BatteryStatus], int]
|
||||
|
||||
read_lo = read_bool(register, led * 2)
|
||||
read_hi = read_bool(register, led * 2 + 1)
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> int
|
||||
|
||||
lo = read_lo(status)
|
||||
hi = read_hi(status)
|
||||
|
||||
if hi:
|
||||
if lo:
|
||||
return LedState.blinking_fast
|
||||
else:
|
||||
return LedState.blinking_slow
|
||||
else:
|
||||
if lo:
|
||||
return LedState.on
|
||||
else:
|
||||
return LedState.off
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def read_bitmap(register):
|
||||
# type: (int) -> Callable[[BatteryStatus], bitmap]
|
||||
|
||||
def get_value(status):
|
||||
# type: (BatteryStatus) -> bitmap
|
||||
value = status.modbus_data[register - cfg.BASE_ADDRESS]
|
||||
return value
|
||||
|
||||
return get_value
|
||||
|
||||
|
||||
def append_unit(unit):
|
||||
# type: (unicode) -> Callable[[unicode], unicode]
|
||||
|
||||
def get_text(v):
|
||||
# type: (unicode) -> unicode
|
||||
return "{0}{1}".format(str(v), unit)
|
||||
|
||||
return get_text
|
||||
|
||||
|
||||
def mean(numbers):
|
||||
# type: (Iterable[float] | Iterable[int]) -> float
|
||||
return float("{:.2f}".format(float(sum(numbers)) / len(numbers)))
|
||||
|
||||
def ssum(numbers):
|
||||
# type: (Iterable[float] | Iterable[int]) -> float
|
||||
return float("{:.2f}".format(float(sum(numbers))))
|
||||
|
||||
|
||||
def first(ts):
|
||||
return next(t for t in ts)
|
||||
|
||||
def return_in_list(ts):
|
||||
return ts
|
||||
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import config as cfg
|
||||
from collections import Iterable
|
||||
|
||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||
# noinspection PyUnreachableCode
|
||||
if False:
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class LedState(object):
|
||||
"""
|
||||
from page 6 of the '48TLxxx ModBus Protocol doc'
|
||||
"""
|
||||
off = 0
|
||||
on = 1
|
||||
blinking_slow = 2
|
||||
blinking_fast = 3
|
||||
|
||||
|
||||
class LedColor(object):
|
||||
green = 0
|
||||
amber = 1
|
||||
blue = 2
|
||||
red = 3
|
||||
|
||||
|
||||
|
||||
class CsvSignal(object):
|
||||
def __init__(self, name, get_value, get_text = None):
|
||||
self.name = name
|
||||
self.get_value = get_value if callable(get_value) else lambda _: get_value
|
||||
self.get_text = get_text
|
||||
|
||||
if get_text is None:
|
||||
self.get_text = ""
|
||||
|
||||
class Signal(object):
|
||||
|
||||
def __init__(self, dbus_path, aggregate, get_value, get_text=None):
|
||||
# type: (str, Callable[[Iterable[object]],object], Callable[[BatteryStatus],object] | object, Callable[[object],unicode] | object)->None
|
||||
"""
|
||||
A Signal holds all information necessary for the handling of a
|
||||
certain datum (e.g. voltage) published by the battery.
|
||||
|
||||
:param dbus_path: str
|
||||
object_path on DBus where the datum needs to be published
|
||||
|
||||
:param aggregate: Iterable[object] -> object
|
||||
function that combines the values of multiple batteries into one.
|
||||
e.g. sum for currents, or mean for voltages
|
||||
|
||||
:param get_value: (BatteryStatus) -> object
|
||||
function to extract the datum from the modbus record,
|
||||
alternatively: a constant
|
||||
|
||||
:param get_text: (object) -> unicode [optional]
|
||||
function to render datum to text, needed by DBus
|
||||
alternatively: a constant
|
||||
"""
|
||||
|
||||
self.dbus_path = dbus_path
|
||||
self.aggregate = aggregate
|
||||
self.get_value = get_value if callable(get_value) else lambda _: get_value
|
||||
self.get_text = get_text if callable(get_text) else lambda _: str(get_text)
|
||||
|
||||
# if no 'get_text' provided use 'default_text' if available, otherwise str()
|
||||
if get_text is None:
|
||||
self.get_text = str
|
||||
|
||||
|
||||
class Battery(object):
|
||||
|
||||
""" Data record to hold hardware and firmware specs of the battery """
|
||||
|
||||
def __init__(self, slave_address, hardware_version, firmware_version, bms_version, ampere_hours):
|
||||
# type: (int, str, str, str, int) -> None
|
||||
self.slave_address = slave_address
|
||||
self.hardware_version = hardware_version
|
||||
self.firmware_version = firmware_version
|
||||
self.bms_version = bms_version
|
||||
self.ampere_hours = ampere_hours
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return 'slave address = {0}\nhardware version = {1}\nfirmware version = {2}\nbms version = {3}\nampere hours = {4}'.format(
|
||||
self.slave_address, self.hardware_version, self.firmware_version, self.bms_version, str(self.ampere_hours))
|
||||
|
||||
|
||||
class BatteryStatus(object):
|
||||
"""
|
||||
record holding the current status of a battery
|
||||
"""
|
||||
def __init__(self, battery, modbus_data):
|
||||
# type: (Battery, list[int]) -> None
|
||||
|
||||
self.battery = battery
|
||||
self.modbus_data = modbus_data
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,276 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
from traceback import print_exc
|
||||
from os import _exit as os_exit
|
||||
from os import statvfs
|
||||
from subprocess import check_output, CalledProcessError
|
||||
import logging
|
||||
import dbus
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)
|
||||
|
||||
class NoVrmPortalIdError(Exception):
|
||||
pass
|
||||
|
||||
# Use this function to make sure the code quits on an unexpected exception. Make sure to use it
|
||||
# when using GLib.idle_add and also GLib.timeout_add.
|
||||
# Without this, the code will just keep running, since GLib does not stop the mainloop on an
|
||||
# exception.
|
||||
# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2)
|
||||
def exit_on_error(func, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except:
|
||||
try:
|
||||
print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit')
|
||||
print_exc()
|
||||
except:
|
||||
pass
|
||||
|
||||
# sys.exit() is not used, since that throws an exception, which does not lead to a program
|
||||
# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.
|
||||
os_exit(1)
|
||||
|
||||
|
||||
__vrm_portal_id = None
|
||||
def get_vrm_portal_id():
|
||||
# The original definition of the VRM Portal ID is that it is the mac
|
||||
# address of the onboard- ethernet port (eth0), stripped from its colons
|
||||
# (:) and lower case. This may however differ between platforms. On Venus
|
||||
# the task is therefore deferred to /sbin/get-unique-id so that a
|
||||
# platform specific method can be easily defined.
|
||||
#
|
||||
# If /sbin/get-unique-id does not exist, then use the ethernet address
|
||||
# of eth0. This also handles the case where velib_python is used as a
|
||||
# package install on a Raspberry Pi.
|
||||
#
|
||||
# On a Linux host where the network interface may not be eth0, you can set
|
||||
# the VRM_IFACE environment variable to the correct name.
|
||||
|
||||
global __vrm_portal_id
|
||||
|
||||
if __vrm_portal_id:
|
||||
return __vrm_portal_id
|
||||
|
||||
portal_id = None
|
||||
|
||||
# First try the method that works if we don't have a data partition. This
|
||||
# will fail when the current user is not root.
|
||||
try:
|
||||
portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip()
|
||||
if not portal_id:
|
||||
raise NoVrmPortalIdError("get-unique-id returned blank")
|
||||
__vrm_portal_id = portal_id
|
||||
return portal_id
|
||||
except CalledProcessError:
|
||||
# get-unique-id returned non-zero
|
||||
raise NoVrmPortalIdError("get-unique-id returned non-zero")
|
||||
except OSError:
|
||||
# File doesn't exist, use fallback
|
||||
pass
|
||||
|
||||
# Fall back to getting our id using a syscall. Assume we are on linux.
|
||||
# Allow the user to override what interface is used using an environment
|
||||
# variable.
|
||||
import fcntl, socket, struct, os
|
||||
|
||||
iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii')
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15]))
|
||||
except IOError:
|
||||
raise NoVrmPortalIdError("ioctl failed for eth0")
|
||||
|
||||
__vrm_portal_id = info[18:24].hex()
|
||||
return __vrm_portal_id
|
||||
|
||||
|
||||
# See VE.Can registers - public.docx for definition of this conversion
|
||||
def convert_vreg_version_to_readable(version):
|
||||
def str_to_arr(x, length):
|
||||
a = []
|
||||
for i in range(0, len(x), length):
|
||||
a.append(x[i:i+length])
|
||||
return a
|
||||
|
||||
x = "%x" % version
|
||||
x = x.upper()
|
||||
|
||||
if len(x) == 5 or len(x) == 3 or len(x) == 1:
|
||||
x = '0' + x
|
||||
|
||||
a = str_to_arr(x, 2);
|
||||
|
||||
# remove the first 00 if there are three bytes and it is 00
|
||||
if len(a) == 3 and a[0] == '00':
|
||||
a.remove(0);
|
||||
|
||||
# if we have two or three bytes now, and the first character is a 0, remove it
|
||||
if len(a) >= 2 and a[0][0:1] == '0':
|
||||
a[0] = a[0][1];
|
||||
|
||||
result = ''
|
||||
for item in a:
|
||||
result += ('.' if result != '' else '') + item
|
||||
|
||||
|
||||
result = 'v' + result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_free_space(path):
|
||||
result = -1
|
||||
|
||||
try:
|
||||
s = statvfs(path)
|
||||
result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users
|
||||
except Exception as ex:
|
||||
logger.info("Error while retrieving free space for path %s: %s" % (path, ex))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _get_sysfs_machine_name():
|
||||
try:
|
||||
with open('/sys/firmware/devicetree/base/model', 'r') as f:
|
||||
return f.read().rstrip('\x00')
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
# Returns None if it cannot find a machine name. Otherwise returns the string
|
||||
# containing the name
|
||||
def get_machine_name():
|
||||
# First try calling the venus utility script
|
||||
try:
|
||||
return check_output("/usr/bin/product-name").strip().decode('UTF-8')
|
||||
except (CalledProcessError, OSError):
|
||||
pass
|
||||
|
||||
# Fall back to sysfs
|
||||
name = _get_sysfs_machine_name()
|
||||
if name is not None:
|
||||
return name
|
||||
|
||||
# Fall back to venus build machine name
|
||||
try:
|
||||
with open('/etc/venus/machine', 'r', encoding='UTF-8') as f:
|
||||
return f.read().strip()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_product_id():
|
||||
""" Find the machine ID and return it. """
|
||||
|
||||
# First try calling the venus utility script
|
||||
try:
|
||||
return check_output("/usr/bin/product-id").strip().decode('UTF-8')
|
||||
except (CalledProcessError, OSError):
|
||||
pass
|
||||
|
||||
# Fall back machine name mechanism
|
||||
name = _get_sysfs_machine_name()
|
||||
return {
|
||||
'Color Control GX': 'C001',
|
||||
'Venus GX': 'C002',
|
||||
'Octo GX': 'C006',
|
||||
'EasySolar-II': 'C007',
|
||||
'MultiPlus-II': 'C008',
|
||||
'Maxi GX': 'C009',
|
||||
'Cerbo GX': 'C00A'
|
||||
}.get(name, 'C003') # C003 is Generic
|
||||
|
||||
|
||||
# Returns False if it cannot open the file. Otherwise returns its rstripped contents
|
||||
def read_file(path):
|
||||
content = False
|
||||
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
content = f.read().rstrip()
|
||||
except Exception as ex:
|
||||
logger.debug("Error while reading %s: %s" % (path, ex))
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def wrap_dbus_value(value):
|
||||
if value is None:
|
||||
return VEDBUS_INVALID
|
||||
if isinstance(value, float):
|
||||
return dbus.Double(value, variant_level=1)
|
||||
if isinstance(value, bool):
|
||||
return dbus.Boolean(value, variant_level=1)
|
||||
if isinstance(value, int):
|
||||
try:
|
||||
return dbus.Int32(value, variant_level=1)
|
||||
except OverflowError:
|
||||
return dbus.Int64(value, variant_level=1)
|
||||
if isinstance(value, str):
|
||||
return dbus.String(value, variant_level=1)
|
||||
if isinstance(value, list):
|
||||
if len(value) == 0:
|
||||
# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.
|
||||
# A (signed) integer is dangerous, because an empty list of signed integers is used to encode
|
||||
# an invalid value.
|
||||
return dbus.Array([], signature=dbus.Signature('u'), variant_level=1)
|
||||
return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)
|
||||
if isinstance(value, dict):
|
||||
# Wrapping the keys of the dictionary causes D-Bus errors like:
|
||||
# 'arguments to dbus_message_iter_open_container() were incorrect,
|
||||
# assertion "(type == DBUS_TYPE_ARRAY && contained_signature &&
|
||||
# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||
|
||||
# _dbus_check_is_valid_signature (contained_signature))" failed in file ...'
|
||||
return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)
|
||||
return value
|
||||
|
||||
|
||||
dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)
|
||||
|
||||
|
||||
def unwrap_dbus_value(val):
|
||||
"""Converts D-Bus values back to the original type. For example if val is of type DBus.Double,
|
||||
a float will be returned."""
|
||||
if isinstance(val, dbus_int_types):
|
||||
return int(val)
|
||||
if isinstance(val, dbus.Double):
|
||||
return float(val)
|
||||
if isinstance(val, dbus.Array):
|
||||
v = [unwrap_dbus_value(x) for x in val]
|
||||
return None if len(v) == 0 else v
|
||||
if isinstance(val, (dbus.Signature, dbus.String)):
|
||||
return str(val)
|
||||
# Python has no byte type, so we convert to an integer.
|
||||
if isinstance(val, dbus.Byte):
|
||||
return int(val)
|
||||
if isinstance(val, dbus.ByteArray):
|
||||
return "".join([bytes(x) for x in val])
|
||||
if isinstance(val, (list, tuple)):
|
||||
return [unwrap_dbus_value(x) for x in val]
|
||||
if isinstance(val, (dbus.Dictionary, dict)):
|
||||
# Do not unwrap the keys, see comment in wrap_dbus_value
|
||||
return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])
|
||||
if isinstance(val, dbus.Boolean):
|
||||
return bool(val)
|
||||
return val
|
||||
|
||||
# When supported, only name owner changes for the the given namespace are reported. This
|
||||
# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily.
|
||||
def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"):
|
||||
# support for arg0namespace is submitted upstream, but not included at the time of
|
||||
# writing, Venus OS does support it, so try if it works.
|
||||
if namespace is None:
|
||||
dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')
|
||||
else:
|
||||
try:
|
||||
dbus.add_signal_receiver(name_owner_changed,
|
||||
signal_name='NameOwnerChanged', arg0namespace=namespace)
|
||||
except TypeError:
|
||||
dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')
|
|
@ -0,0 +1,614 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import dbus.service
|
||||
import logging
|
||||
import traceback
|
||||
import os
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
from ve_utils import wrap_dbus_value, unwrap_dbus_value
|
||||
|
||||
# vedbus contains three classes:
|
||||
# VeDbusItemImport -> use this to read data from the dbus, ie import
|
||||
# VeDbusItemExport -> use this to export data to the dbus (one value)
|
||||
# VeDbusService -> use that to create a service and export several values to the dbus
|
||||
|
||||
# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.
|
||||
# All projects that used busitem.py need to migrate to this package. And some
|
||||
# projects used to define there own equivalent of VeDbusItemExport. Better to
|
||||
# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.
|
||||
|
||||
# TODOS
|
||||
# 1 check for datatypes, it works now, but not sure if all is compliant with
|
||||
# com.victronenergy.BusItem interface definition. See also the files in
|
||||
# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps
|
||||
# something similar should also be done in VeDbusBusItemExport?
|
||||
# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?
|
||||
# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking
|
||||
# changes possible. Does everybody first invalidate its data before leaving the bus?
|
||||
# And what about before taking one object away from the bus, instead of taking the
|
||||
# whole service offline?
|
||||
# They should! And after taking one value away, do we need to know that someone left
|
||||
# the bus? Or we just keep that value in invalidated for ever? Result is that we can't
|
||||
# see the difference anymore between an invalidated value and a value that was first on
|
||||
# the bus and later not anymore. See comments above VeDbusItemImport as well.
|
||||
# 9 there are probably more todos in the code below.
|
||||
|
||||
# Some thoughts with regards to the data types:
|
||||
#
|
||||
# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types
|
||||
# ---
|
||||
# Variants are represented by setting the variant_level keyword argument in the
|
||||
# constructor of any D-Bus data type to a value greater than 0 (variant_level 1
|
||||
# means a variant containing some other data type, variant_level 2 means a variant
|
||||
# containing a variant containing some other data type, and so on). If a non-variant
|
||||
# is passed as an argument but introspection indicates that a variant is expected,
|
||||
# it'll automatically be wrapped in a variant.
|
||||
# ---
|
||||
#
|
||||
# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass
|
||||
# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera
|
||||
#
|
||||
# So all together that explains why we don't need to explicitly convert back and forth
|
||||
# between the dbus datatypes and the standard python datatypes. Note that all datatypes
|
||||
# in python are objects. Even an int is an object.
|
||||
|
||||
# The signature of a variant is 'v'.
|
||||
|
||||
# Export ourselves as a D-Bus service.
|
||||
class VeDbusService(object):
|
||||
def __init__(self, servicename, bus=None):
|
||||
# dict containing the VeDbusItemExport objects, with their path as the key.
|
||||
self._dbusobjects = {}
|
||||
self._dbusnodes = {}
|
||||
self._ratelimiters = []
|
||||
self._dbusname = None
|
||||
|
||||
# dict containing the onchange callbacks, for each object. Object path is the key
|
||||
self._onchangecallbacks = {}
|
||||
|
||||
# Connect to session bus whenever present, else use the system bus
|
||||
self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())
|
||||
|
||||
# make the dbus connection available to outside, could make this a true property instead, but ach..
|
||||
self.dbusconn = self._dbusconn
|
||||
|
||||
# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)
|
||||
self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True)
|
||||
|
||||
# Add the root item that will return all items as a tree
|
||||
self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self)
|
||||
|
||||
logging.info("registered ourselves on D-Bus as %s" % servicename)
|
||||
|
||||
# To force immediate deregistering of this dbus service and all its object paths, explicitly
|
||||
# call __del__().
|
||||
def __del__(self):
|
||||
for node in list(self._dbusnodes.values()):
|
||||
node.__del__()
|
||||
self._dbusnodes.clear()
|
||||
for item in list(self._dbusobjects.values()):
|
||||
item.__del__()
|
||||
self._dbusobjects.clear()
|
||||
if self._dbusname:
|
||||
self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code
|
||||
self._dbusname = None
|
||||
|
||||
def get_name(self):
|
||||
return self._dbusname.get_name()
|
||||
|
||||
# @param callbackonchange function that will be called when this value is changed. First parameter will
|
||||
# be the path of the object, second the new value. This callback should return
|
||||
# True to accept the change, False to reject it.
|
||||
def add_path(self, path, value, description="", writeable=False,
|
||||
onchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None):
|
||||
|
||||
if onchangecallback is not None:
|
||||
self._onchangecallbacks[path] = onchangecallback
|
||||
|
||||
itemtype = itemtype or VeDbusItemExport
|
||||
item = itemtype(self._dbusconn, path, value, description, writeable,
|
||||
self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype)
|
||||
|
||||
spl = path.split('/')
|
||||
for i in range(2, len(spl)):
|
||||
subPath = '/'.join(spl[:i])
|
||||
if subPath not in self._dbusnodes and subPath not in self._dbusobjects:
|
||||
self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self)
|
||||
self._dbusobjects[path] = item
|
||||
logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))
|
||||
|
||||
# Add the mandatory paths, as per victron dbus api doc
|
||||
def add_mandatory_paths(self, processname, processversion, connection,
|
||||
deviceinstance, productid, productname, firmwareversion, hardwareversion, connected):
|
||||
self.add_path('/Mgmt/ProcessName', processname)
|
||||
self.add_path('/Mgmt/ProcessVersion', processversion)
|
||||
self.add_path('/Mgmt/Connection', connection)
|
||||
|
||||
# Create rest of the mandatory objects
|
||||
self.add_path('/DeviceInstance', deviceinstance)
|
||||
self.add_path('/ProductId', productid)
|
||||
self.add_path('/ProductName', productname)
|
||||
self.add_path('/FirmwareVersion', firmwareversion)
|
||||
self.add_path('/HardwareVersion', hardwareversion)
|
||||
self.add_path('/Connected', connected)
|
||||
|
||||
# Callback function that is called from the VeDbusItemExport objects when a value changes. This function
|
||||
# maps the change-request to the onchangecallback given to us for this specific path.
|
||||
def _value_changed(self, path, newvalue):
|
||||
if path not in self._onchangecallbacks:
|
||||
return True
|
||||
|
||||
return self._onchangecallbacks[path](path, newvalue)
|
||||
|
||||
def _item_deleted(self, path):
|
||||
self._dbusobjects.pop(path)
|
||||
for np in list(self._dbusnodes.keys()):
|
||||
if np != '/':
|
||||
for ip in self._dbusobjects:
|
||||
if ip.startswith(np + '/'):
|
||||
break
|
||||
else:
|
||||
self._dbusnodes[np].__del__()
|
||||
self._dbusnodes.pop(np)
|
||||
|
||||
def __getitem__(self, path):
|
||||
return self._dbusobjects[path].local_get_value()
|
||||
|
||||
def __setitem__(self, path, newvalue):
|
||||
self._dbusobjects[path].local_set_value(newvalue)
|
||||
|
||||
def __delitem__(self, path):
|
||||
self._dbusobjects[path].__del__() # Invalidates and then removes the object path
|
||||
assert path not in self._dbusobjects
|
||||
|
||||
def __contains__(self, path):
|
||||
return path in self._dbusobjects
|
||||
|
||||
def __enter__(self):
|
||||
l = ServiceContext(self)
|
||||
self._ratelimiters.append(l)
|
||||
return l
|
||||
|
||||
def __exit__(self, *exc):
|
||||
# pop off the top one and flush it. If with statements are nested
|
||||
# then each exit flushes its own part.
|
||||
if self._ratelimiters:
|
||||
self._ratelimiters.pop().flush()
|
||||
|
||||
class ServiceContext(object):
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.changes = {}
|
||||
|
||||
def __getitem__(self, path):
|
||||
return self.parent[path]
|
||||
|
||||
def __setitem__(self, path, newvalue):
|
||||
c = self.parent._dbusobjects[path]._local_set_value(newvalue)
|
||||
if c is not None:
|
||||
self.changes[path] = c
|
||||
|
||||
def flush(self):
|
||||
if self.changes:
|
||||
self.parent._dbusnodes['/'].ItemsChanged(self.changes)
|
||||
|
||||
class TrackerDict(defaultdict):
|
||||
""" Same as defaultdict, but passes the key to default_factory. """
|
||||
def __missing__(self, key):
|
||||
self[key] = x = self.default_factory(key)
|
||||
return x
|
||||
|
||||
class VeDbusRootTracker(object):
|
||||
""" This tracks the root of a dbus path and listens for PropertiesChanged
|
||||
signals. When a signal arrives, parse it and unpack the key/value changes
|
||||
into traditional events, then pass it to the original eventCallback
|
||||
method. """
|
||||
def __init__(self, bus, serviceName):
|
||||
self.importers = defaultdict(weakref.WeakSet)
|
||||
self.serviceName = serviceName
|
||||
self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal(
|
||||
"ItemsChanged", weak_functor(self._items_changed_handler))
|
||||
|
||||
def __del__(self):
|
||||
self._match.remove()
|
||||
self._match = None
|
||||
|
||||
def add(self, i):
|
||||
self.importers[i.path].add(i)
|
||||
|
||||
def _items_changed_handler(self, items):
|
||||
if not isinstance(items, dict):
|
||||
return
|
||||
|
||||
for path, changes in items.items():
|
||||
try:
|
||||
v = changes['Value']
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
try:
|
||||
t = changes['Text']
|
||||
except KeyError:
|
||||
t = str(unwrap_dbus_value(v))
|
||||
|
||||
for i in self.importers.get(path, ()):
|
||||
i._properties_changed_handler({'Value': v, 'Text': t})
|
||||
|
||||
"""
|
||||
Importing basics:
|
||||
- If when we power up, the D-Bus service does not exist, or it does exist and the path does not
|
||||
yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its
|
||||
initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,
|
||||
call the eventCallback.
|
||||
- If when we power up, save it
|
||||
- When using get_value, know that there is no difference between services (or object paths) that don't
|
||||
exist and paths that are invalid (= empty array, see above). Both will return None. In case you do
|
||||
really want to know ifa path exists or not, use the exists property.
|
||||
- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals
|
||||
with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-
|
||||
signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this
|
||||
class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this
|
||||
class.
|
||||
|
||||
Read when using this class:
|
||||
Note that when a service leaves that D-Bus without invalidating all its exported objects first, for
|
||||
example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,
|
||||
make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,
|
||||
because that takes care of all of that for you.
|
||||
"""
|
||||
class VeDbusItemImport(object):
|
||||
def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True):
|
||||
instance = object.__new__(cls)
|
||||
|
||||
# If signal tracking should be done, also add to root tracker
|
||||
if createsignal:
|
||||
if "_roots" not in cls.__dict__:
|
||||
cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k))
|
||||
|
||||
return instance
|
||||
|
||||
## Constructor
|
||||
# @param bus the bus-object (SESSION or SYSTEM).
|
||||
# @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'
|
||||
# @param path the object-path, for example '/Dc/V'
|
||||
# @param eventCallback function that you want to be called on a value change
|
||||
# @param createSignal only set this to False if you use this function to one time read a value. When
|
||||
# leaving it to True, make sure to also subscribe to the NameOwnerChanged signal
|
||||
# elsewhere. See also note some 15 lines up.
|
||||
def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):
|
||||
# TODO: is it necessary to store _serviceName and _path? Isn't it
|
||||
# stored in the bus_getobjectsomewhere?
|
||||
self._serviceName = serviceName
|
||||
self._path = path
|
||||
self._match = None
|
||||
# TODO: _proxy is being used in settingsdevice.py, make a getter for that
|
||||
self._proxy = bus.get_object(serviceName, path, introspect=False)
|
||||
self.eventCallback = eventCallback
|
||||
|
||||
assert eventCallback is None or createsignal == True
|
||||
if createsignal:
|
||||
self._match = self._proxy.connect_to_signal(
|
||||
"PropertiesChanged", weak_functor(self._properties_changed_handler))
|
||||
self._roots[serviceName].add(self)
|
||||
|
||||
# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to
|
||||
# None, same as when a value is invalid
|
||||
self._cachedvalue = None
|
||||
try:
|
||||
v = self._proxy.GetValue()
|
||||
except dbus.exceptions.DBusException:
|
||||
pass
|
||||
else:
|
||||
self._cachedvalue = unwrap_dbus_value(v)
|
||||
|
||||
def __del__(self):
|
||||
if self._match is not None:
|
||||
self._match.remove()
|
||||
self._match = None
|
||||
self._proxy = None
|
||||
|
||||
def _refreshcachedvalue(self):
|
||||
self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())
|
||||
|
||||
## Returns the path as a string, for example '/AC/L1/V'
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1
|
||||
@property
|
||||
def serviceName(self):
|
||||
return self._serviceName
|
||||
|
||||
## Returns the value of the dbus-item.
|
||||
# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)
|
||||
# this is not a property to keep the name consistant with the com.victronenergy.busitem interface
|
||||
# returns None when the property is invalid
|
||||
def get_value(self):
|
||||
return self._cachedvalue
|
||||
|
||||
## Writes a new value to the dbus-item
|
||||
def set_value(self, newvalue):
|
||||
r = self._proxy.SetValue(wrap_dbus_value(newvalue))
|
||||
|
||||
# instead of just saving the value, go to the dbus and get it. So we have the right type etc.
|
||||
if r == 0:
|
||||
self._refreshcachedvalue()
|
||||
|
||||
return r
|
||||
|
||||
## Resets the item to its default value
|
||||
def set_default(self):
|
||||
self._proxy.SetDefault()
|
||||
self._refreshcachedvalue()
|
||||
|
||||
## Returns the text representation of the value.
|
||||
# For example when the value is an enum/int GetText might return the string
|
||||
# belonging to that enum value. Another example, for a voltage, GetValue
|
||||
# would return a float, 12.0Volt, and GetText could return 12 VDC.
|
||||
#
|
||||
# Note that this depends on how the dbus-producer has implemented this.
|
||||
def get_text(self):
|
||||
return self._proxy.GetText()
|
||||
|
||||
## Returns true of object path exists, and false if it doesn't
|
||||
@property
|
||||
def exists(self):
|
||||
# TODO: do some real check instead of this crazy thing.
|
||||
r = False
|
||||
try:
|
||||
r = self._proxy.GetValue()
|
||||
r = True
|
||||
except dbus.exceptions.DBusException:
|
||||
pass
|
||||
|
||||
return r
|
||||
|
||||
## callback for the trigger-event.
|
||||
# @param eventCallback the event-callback-function.
|
||||
@property
|
||||
def eventCallback(self):
|
||||
return self._eventCallback
|
||||
|
||||
@eventCallback.setter
|
||||
def eventCallback(self, eventCallback):
|
||||
self._eventCallback = eventCallback
|
||||
|
||||
## Is called when the value of the imported bus-item changes.
|
||||
# Stores the new value in our local cache, and calls the eventCallback, if set.
|
||||
def _properties_changed_handler(self, changes):
|
||||
if "Value" in changes:
|
||||
changes['Value'] = unwrap_dbus_value(changes['Value'])
|
||||
self._cachedvalue = changes['Value']
|
||||
if self._eventCallback:
|
||||
# The reason behind this try/except is to prevent errors silently ending up the an error
|
||||
# handler in the dbus code.
|
||||
try:
|
||||
self._eventCallback(self._serviceName, self._path, changes)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
os._exit(1) # sys.exit() is not used, since that also throws an exception
|
||||
|
||||
|
||||
class VeDbusTreeExport(dbus.service.Object):
|
||||
def __init__(self, bus, objectPath, service):
|
||||
dbus.service.Object.__init__(self, bus, objectPath)
|
||||
self._service = service
|
||||
logging.debug("VeDbusTreeExport %s has been created" % objectPath)
|
||||
|
||||
def __del__(self):
|
||||
# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,
|
||||
# so we need a copy.
|
||||
path = self._get_path()
|
||||
if path is None:
|
||||
return
|
||||
self.remove_from_connection()
|
||||
logging.debug("VeDbusTreeExport %s has been removed" % path)
|
||||
|
||||
def _get_path(self):
|
||||
if len(self._locations) == 0:
|
||||
return None
|
||||
return self._locations[0][1]
|
||||
|
||||
def _get_value_handler(self, path, get_text=False):
|
||||
logging.debug("_get_value_handler called for %s" % path)
|
||||
r = {}
|
||||
px = path
|
||||
if not px.endswith('/'):
|
||||
px += '/'
|
||||
for p, item in self._service._dbusobjects.items():
|
||||
if p.startswith(px):
|
||||
v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())
|
||||
r[p[len(px):]] = v
|
||||
logging.debug(r)
|
||||
return r
|
||||
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
|
||||
def GetValue(self):
|
||||
value = self._get_value_handler(self._get_path())
|
||||
return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)
|
||||
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
|
||||
def GetText(self):
|
||||
return self._get_value_handler(self._get_path(), True)
|
||||
|
||||
def local_get_value(self):
|
||||
return self._get_value_handler(self.path)
|
||||
|
||||
class VeDbusRootExport(VeDbusTreeExport):
|
||||
@dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}')
|
||||
def ItemsChanged(self, changes):
|
||||
pass
|
||||
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}')
|
||||
def GetItems(self):
|
||||
return {
|
||||
path: {
|
||||
'Value': wrap_dbus_value(item.local_get_value()),
|
||||
'Text': item.GetText() }
|
||||
for path, item in self._service._dbusobjects.items()
|
||||
}
|
||||
|
||||
|
||||
class VeDbusItemExport(dbus.service.Object):
|
||||
## Constructor of VeDbusItemExport
|
||||
#
|
||||
# Use this object to export (publish), values on the dbus
|
||||
# Creates the dbus-object under the given dbus-service-name.
|
||||
# @param bus The dbus object.
|
||||
# @param objectPath The dbus-object-path.
|
||||
# @param value Value to initialize ourselves with, defaults to None which means Invalid
|
||||
# @param description String containing a description. Can be called over the dbus with GetDescription()
|
||||
# @param writeable what would this do!? :).
|
||||
# @param callback Function that will be called when someone else changes the value of this VeBusItem
|
||||
# over the dbus. First parameter passed to callback will be our path, second the new
|
||||
# value. This callback should return True to accept the change, False to reject it.
|
||||
def __init__(self, bus, objectPath, value=None, description=None, writeable=False,
|
||||
onchangecallback=None, gettextcallback=None, deletecallback=None,
|
||||
valuetype=None):
|
||||
dbus.service.Object.__init__(self, bus, objectPath)
|
||||
self._onchangecallback = onchangecallback
|
||||
self._gettextcallback = gettextcallback
|
||||
self._value = value
|
||||
self._description = description
|
||||
self._writeable = writeable
|
||||
self._deletecallback = deletecallback
|
||||
self._type = valuetype
|
||||
|
||||
# To force immediate deregistering of this dbus object, explicitly call __del__().
|
||||
def __del__(self):
|
||||
# self._get_path() will raise an exception when retrieved after the
|
||||
# call to .remove_from_connection, so we need a copy.
|
||||
path = self._get_path()
|
||||
if path == None:
|
||||
return
|
||||
if self._deletecallback is not None:
|
||||
self._deletecallback(path)
|
||||
self.remove_from_connection()
|
||||
logging.debug("VeDbusItemExport %s has been removed" % path)
|
||||
|
||||
def _get_path(self):
|
||||
if len(self._locations) == 0:
|
||||
return None
|
||||
return self._locations[0][1]
|
||||
|
||||
## Sets the value. And in case the value is different from what it was, a signal
|
||||
# will be emitted to the dbus. This function is to be used in the python code that
|
||||
# is using this class to export values to the dbus.
|
||||
# set value to None to indicate that it is Invalid
|
||||
def local_set_value(self, newvalue):
|
||||
changes = self._local_set_value(newvalue)
|
||||
if changes is not None:
|
||||
self.PropertiesChanged(changes)
|
||||
|
||||
def _local_set_value(self, newvalue):
|
||||
if self._value == newvalue:
|
||||
return None
|
||||
|
||||
self._value = newvalue
|
||||
return {
|
||||
'Value': wrap_dbus_value(newvalue),
|
||||
'Text': self.GetText()
|
||||
}
|
||||
|
||||
def local_get_value(self):
|
||||
return self._value
|
||||
|
||||
# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====
|
||||
|
||||
## Dbus exported method SetValue
|
||||
# Function is called over the D-Bus by other process. It will first check (via callback) if new
|
||||
# value is accepted. And it is, stores it and emits a changed-signal.
|
||||
# @param value The new value.
|
||||
# @return completion-code When successful a 0 is return, and when not a -1 is returned.
|
||||
@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')
|
||||
def SetValue(self, newvalue):
|
||||
if not self._writeable:
|
||||
return 1 # NOT OK
|
||||
|
||||
newvalue = unwrap_dbus_value(newvalue)
|
||||
|
||||
# If value type is enforced, cast it. If the type can be coerced
|
||||
# python will do it for us. This allows ints to become floats,
|
||||
# or bools to become ints. Additionally also allow None, so that
|
||||
# a path may be invalidated.
|
||||
if self._type is not None and newvalue is not None:
|
||||
try:
|
||||
newvalue = self._type(newvalue)
|
||||
except (ValueError, TypeError):
|
||||
return 1 # NOT OK
|
||||
|
||||
if newvalue == self._value:
|
||||
return 0 # OK
|
||||
|
||||
# call the callback given to us, and check if new value is OK.
|
||||
if (self._onchangecallback is None or
|
||||
(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):
|
||||
|
||||
self.local_set_value(newvalue)
|
||||
return 0 # OK
|
||||
|
||||
return 2 # NOT OK
|
||||
|
||||
## Dbus exported method GetDescription
|
||||
#
|
||||
# Returns the a description.
|
||||
# @param language A language code (e.g. ISO 639-1 en-US).
|
||||
# @param length Lenght of the language string.
|
||||
# @return description
|
||||
@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')
|
||||
def GetDescription(self, language, length):
|
||||
return self._description if self._description is not None else 'No description given'
|
||||
|
||||
## Dbus exported method GetValue
|
||||
# Returns the value.
|
||||
# @return the value when valid, and otherwise an empty array
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='v')
|
||||
def GetValue(self):
|
||||
return wrap_dbus_value(self._value)
|
||||
|
||||
## Dbus exported method GetText
|
||||
# Returns the value as string of the dbus-object-path.
|
||||
# @return text A text-value. '---' when local value is invalid
|
||||
@dbus.service.method('com.victronenergy.BusItem', out_signature='s')
|
||||
def GetText(self):
|
||||
if self._value is None:
|
||||
return '---'
|
||||
|
||||
# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we
|
||||
# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from
|
||||
# the application itself, as all data from the D-Bus should have been unwrapped by now.
|
||||
if self._gettextcallback is None and type(self._value) == dbus.Byte:
|
||||
return str(int(self._value))
|
||||
|
||||
if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':
|
||||
return "0x%X" % self._value
|
||||
|
||||
if self._gettextcallback is None:
|
||||
return str(self._value)
|
||||
|
||||
return self._gettextcallback(self.__dbus_object_path__, self._value)
|
||||
|
||||
## The signal that indicates that the value has changed.
|
||||
# Other processes connected to this BusItem object will have subscribed to the
|
||||
# event when they want to track our state.
|
||||
@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')
|
||||
def PropertiesChanged(self, changes):
|
||||
pass
|
||||
|
||||
## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference
|
||||
## to the object which method is to be called.
|
||||
## Use this object to break circular references.
|
||||
class weak_functor:
|
||||
def __init__(self, f):
|
||||
self._r = weakref.ref(f.__self__)
|
||||
self._f = weakref.ref(f.__func__)
|
||||
|
||||
def __call__(self, *args, **kargs):
|
||||
r = self._r()
|
||||
f = self._f()
|
||||
if r == None or f == None:
|
||||
return
|
||||
f(r, *args, **kargs)
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
. /opt/victronenergy/serial-starter/run-service.sh
|
||||
|
||||
app=/opt/victronenergy/dbus-fzsonick-48tl/dbus-fzsonick-48tl.py
|
||||
args="$tty"
|
||||
start $args
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
**/docs
|
||||
**/examples
|
||||
**/test
|
||||
**/utils
|
||||
setup.py
|
|
@ -0,0 +1,8 @@
|
|||
languages:
|
||||
- python
|
||||
exclude_paths:
|
||||
- docs/*
|
||||
- tests/*
|
||||
- utils/*
|
||||
- pika/examples/*
|
||||
- pika/spec.py
|
|
@ -0,0 +1,2 @@
|
|||
[run]
|
||||
omit = pika/spec.py
|
|
@ -0,0 +1,15 @@
|
|||
Thank you for using Pika.
|
||||
|
||||
GitHub issues are **strictly** used for actionable work and pull
|
||||
requests.
|
||||
|
||||
Pika's maintainers do NOT use GitHub issues for questions, root cause
|
||||
analysis, conversations, code reviews, etc.
|
||||
|
||||
Please direct all non-work issues to either the `pika-python` or
|
||||
`rabbitmq-users` mailing list:
|
||||
|
||||
* https://groups.google.com/forum/#!forum/pika-python
|
||||
* https://groups.google.com/forum/#!forum/rabbitmq-users
|
||||
|
||||
Thank you
|
|
@ -0,0 +1,43 @@
|
|||
## Proposed Changes
|
||||
|
||||
Please describe the big picture of your changes here to communicate to
|
||||
the Pika team why we should accept this pull request. If it fixes a bug
|
||||
or resolves a feature request, be sure to link to that issue.
|
||||
|
||||
A pull request that doesn't explain **why** the change was made has a
|
||||
much lower chance of being accepted.
|
||||
|
||||
If English isn't your first language, don't worry about it and try to
|
||||
communicate the problem you are trying to solve to the best of your
|
||||
abilities. As long as we can understand the intent, it's all good.
|
||||
|
||||
## Types of Changes
|
||||
|
||||
What types of changes does your code introduce to this project?
|
||||
_Put an `x` in the boxes that apply_
|
||||
|
||||
- [ ] Bugfix (non-breaking change which fixes issue #NNNN)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Documentation (correction or otherwise)
|
||||
- [ ] Cosmetics (whitespace, appearance)
|
||||
|
||||
## Checklist
|
||||
|
||||
_Put an `x` in the boxes that apply. You can also fill these out after
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to
|
||||
ask on the
|
||||
[`pika-python`](https://groups.google.com/forum/#!forum/pika-python)
|
||||
mailing list. We're here to help! This is simply a reminder of what we
|
||||
are going to look for before merging your code._
|
||||
|
||||
- [ ] I have read the `CONTRIBUTING.md` document
|
||||
- [ ] All tests pass locally with my changes
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] I have added necessary documentation (if appropriate)
|
||||
|
||||
## Further Comments
|
||||
|
||||
If this is a relatively large or complex change, kick off the discussion
|
||||
by explaining why you chose the solution you did and what alternatives
|
||||
you considered, etc.
|
|
@ -0,0 +1,20 @@
|
|||
*.pyc
|
||||
*~
|
||||
.idea
|
||||
.coverage
|
||||
.tox
|
||||
.DS_Store
|
||||
.python-version
|
||||
pika.iml
|
||||
codegen
|
||||
pika.egg-info
|
||||
debug/
|
||||
examples/pika
|
||||
examples/blocking/pika
|
||||
atlassian*xml
|
||||
build
|
||||
dist
|
||||
docs/_build
|
||||
venv*/
|
||||
env/
|
||||
testdata/*.conf
|
|
@ -0,0 +1,103 @@
|
|||
language: python
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: deb https://packages.erlang-solutions.com/ubuntu trusty contrib
|
||||
key_url: https://packages.erlang-solutions.com/ubuntu/erlang_solutions.asc
|
||||
packages:
|
||||
# apt-cache show erlang-nox=1:20.3-1 | grep Depends | tr ' ' '\n' | grep erlang | grep -v erlang-base-hipe | tr -d ',' | sed 's/$/=1:20.3-1/'
|
||||
- erlang-nox
|
||||
|
||||
env:
|
||||
global:
|
||||
- RABBITMQ_VERSION=3.7.8
|
||||
- RABBITMQ_DOWNLOAD_URL="https://github.com/rabbitmq/rabbitmq-server/releases/download/v$RABBITMQ_VERSION/rabbitmq-server-generic-unix-$RABBITMQ_VERSION.tar.xz"
|
||||
- RABBITMQ_TAR="rabbitmq-$RABBITMQ_VERSION.tar.xz"
|
||||
- PATH=$HOME/.local/bin:$PATH
|
||||
- AWS_DEFAULT_REGION=us-east-1
|
||||
- secure: "Eghft2UgJmWuCgnqz6O+KV5F9AERzUbKIeXkcw7vsFAVdkB9z01XgqVLhQ6N+n6i8mkiRDkc0Jes6htVtO4Hi6lTTFeDhu661YCXXTFdRdsx+D9v5bgw8Q2bP41xFy0iao7otYqkzFKIo32Q2cUYzMUqXlS661Yai5DXldr3mjM="
|
||||
- secure: "LjieH/Yh0ng5gwT6+Pl3rL7RMxxb/wOlogoLG7cS99XKdX6N4WRVFvWbHWwCxoVr0be2AcyQynu4VOn+0jC8iGfQjkJZ7UrJjZCDGWbNjAWrNcY0F9VdretFDy8Vn2sHfBXq8fINqszJkgTnmbQk8dZWUtj0m/RNVnOBeBcsIOU="
|
||||
|
||||
stages:
|
||||
- test
|
||||
- name: coverage
|
||||
if: repo = pika/pika
|
||||
- name: deploy
|
||||
if: tag IS present
|
||||
|
||||
cache:
|
||||
apt: true
|
||||
directories:
|
||||
- $HOME/.cache
|
||||
|
||||
install:
|
||||
- pip install -r test-requirements.txt
|
||||
- pip install awscli==1.11.18
|
||||
- if [ ! -d "$HOME/.cache" ]; then mkdir "$HOME/.cache"; fi
|
||||
- if [ -s "$HOME/.cache/$RABBITMQ_TAR" ]; then echo "[INFO] found cached $RABBITMQ_TAR file"; else wget -O "$HOME/.cache/$RABBITMQ_TAR" "$RABBITMQ_DOWNLOAD_URL"; fi
|
||||
- tar -C "$TRAVIS_BUILD_DIR" -xvf "$HOME/.cache/$RABBITMQ_TAR"
|
||||
- sed -e "s#PIKA_DIR#$TRAVIS_BUILD_DIR#g" "$TRAVIS_BUILD_DIR/testdata/rabbitmq.conf.in" > "$TRAVIS_BUILD_DIR/testdata/rabbitmq.conf"
|
||||
|
||||
before_script:
|
||||
- pip freeze
|
||||
- /bin/sh -c "RABBITMQ_PID_FILE=$TRAVIS_BUILD_DIR/rabbitmq.pid RABBITMQ_CONFIG_FILE=$TRAVIS_BUILD_DIR/testdata/rabbitmq $TRAVIS_BUILD_DIR/rabbitmq_server-$RABBITMQ_VERSION/sbin/rabbitmq-server &"
|
||||
- /bin/sh "$TRAVIS_BUILD_DIR/rabbitmq_server-$RABBITMQ_VERSION/sbin/rabbitmqctl" wait "$TRAVIS_BUILD_DIR/rabbitmq.pid"
|
||||
- /bin/sh "$TRAVIS_BUILD_DIR/rabbitmq_server-$RABBITMQ_VERSION/sbin/rabbitmqctl" status
|
||||
|
||||
script:
|
||||
# See https://github.com/travis-ci/travis-ci/issues/1066 and https://github.com/pika/pika/pull/984#issuecomment-370565220
|
||||
# as to why 'set -e' and 'set +e' are added here
|
||||
- set -e
|
||||
- nosetests
|
||||
- PIKA_TEST_TLS=true nosetests
|
||||
- set +e
|
||||
|
||||
after_success:
|
||||
- aws s3 cp .coverage "s3://com-gavinroy-travis/pika/$TRAVIS_BUILD_NUMBER/.coverage.${TRAVIS_PYTHON_VERSION}"
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- python: pypy3
|
||||
- python: pypy
|
||||
- python: 2.7
|
||||
- python: 3.4
|
||||
- python: 3.5
|
||||
- python: 3.6
|
||||
- python: 3.7
|
||||
dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
|
||||
- stage: coverage
|
||||
if: fork = false OR type != pull_request
|
||||
python: 3.6
|
||||
services: []
|
||||
install:
|
||||
- pip install awscli coverage codecov
|
||||
before_script: []
|
||||
script:
|
||||
- mkdir coverage
|
||||
- aws s3 cp --recursive s3://com-gavinroy-travis/pika/$TRAVIS_BUILD_NUMBER/ coverage
|
||||
- cd coverage
|
||||
- coverage combine
|
||||
- cd ..
|
||||
- mv coverage/.coverage .
|
||||
- coverage report
|
||||
after_success: codecov
|
||||
- stage: deploy
|
||||
if: repo = pika/pika
|
||||
python: 3.6
|
||||
services: []
|
||||
install: true
|
||||
before_script: []
|
||||
script: true
|
||||
after_success: []
|
||||
deploy:
|
||||
distributions: sdist bdist_wheel
|
||||
provider: pypi
|
||||
user: crad
|
||||
on:
|
||||
tags: true
|
||||
all_branches: true
|
||||
password:
|
||||
secure: "V/JTU/X9C6uUUVGEAWmWWbmKW7NzVVlC/JWYpo05Ha9c0YV0vX4jOfov2EUAphM0WwkD/MRhz4dq3kCU5+cjHxR3aTSb+sbiElsCpaciaPkyrns+0wT5MCMO29Lpnq2qBLc1ePR1ey5aTWC/VibgFJOL7H/3wyvukL6ZaCnktYk="
|
|
@ -0,0 +1,760 @@
|
|||
Version History
|
||||
===============
|
||||
|
||||
0.13.1 2019-03-07
|
||||
-----------------
|
||||
|
||||
`GitHub milestone <https://github.com/pika/pika/milestone/14>`_
|
||||
|
||||
0.13.0 2019-01-17
|
||||
-----------------
|
||||
|
||||
`GitHub milestone <https://github.com/pika/pika/milestone/13>`_
|
||||
|
||||
- `AsyncioConnection`, `TornadoConnection` and `TwistedProtocolConnection` are no longer auto-imported (`PR <https://github.com/pika/pika/pull/1129>`_)
|
||||
- Python `3.7` support (`Issue <https://github.com/pika/pika/issues/1107>`_)
|
||||
|
||||
0.12.0 2018-06-19
|
||||
-----------------
|
||||
|
||||
`GitHub milestone <https://github.com/pika/pika/milestone/12>`_
|
||||
|
||||
This is an interim release prior to version `1.0.0`. It includes the following backported pull requests and commits from the `master` branch:
|
||||
|
||||
- `PR #908 <https://github.com/pika/pika/pull/908>`_
|
||||
- `PR #910 <https://github.com/pika/pika/pull/910>`_
|
||||
- `PR #918 <https://github.com/pika/pika/pull/918>`_
|
||||
- `PR #920 <https://github.com/pika/pika/pull/920>`_
|
||||
- `PR #924 <https://github.com/pika/pika/pull/924>`_
|
||||
- `PR #937 <https://github.com/pika/pika/pull/937>`_
|
||||
- `PR #938 <https://github.com/pika/pika/pull/938>`_
|
||||
- `PR #933 <https://github.com/pika/pika/pull/933>`_
|
||||
- `PR #940 <https://github.com/pika/pika/pull/940>`_
|
||||
- `PR #932 <https://github.com/pika/pika/pull/932>`_
|
||||
- `PR #928 <https://github.com/pika/pika/pull/928>`_
|
||||
- `PR #934 <https://github.com/pika/pika/pull/934>`_
|
||||
- `PR #915 <https://github.com/pika/pika/pull/915>`_
|
||||
- `PR #946 <https://github.com/pika/pika/pull/946>`_
|
||||
- `PR #947 <https://github.com/pika/pika/pull/947>`_
|
||||
- `PR #952 <https://github.com/pika/pika/pull/952>`_
|
||||
- `PR #956 <https://github.com/pika/pika/pull/956>`_
|
||||
- `PR #966 <https://github.com/pika/pika/pull/966>`_
|
||||
- `PR #975 <https://github.com/pika/pika/pull/975>`_
|
||||
- `PR #978 <https://github.com/pika/pika/pull/978>`_
|
||||
- `PR #981 <https://github.com/pika/pika/pull/981>`_
|
||||
- `PR #994 <https://github.com/pika/pika/pull/994>`_
|
||||
- `PR #1007 <https://github.com/pika/pika/pull/1007>`_
|
||||
- `PR #1045 <https://github.com/pika/pika/pull/1045>`_ (manually backported)
|
||||
- `PR #1011 <https://github.com/pika/pika/pull/1011>`_
|
||||
|
||||
Commits:
|
||||
|
||||
Travis CI fail fast - 3f0e739
|
||||
|
||||
New features:
|
||||
|
||||
`BlockingConnection` now supports the `add_callback_threadsafe` method which allows a function to be executed correctly on the IO loop thread. The main use-case for this is as follows:
|
||||
|
||||
- Application sets up a thread for `BlockingConnection` and calls `basic_consume` on it
|
||||
- When a message is received, work is done on another thread
|
||||
- When the work is done, the worker uses `connection.add_callback_threadsafe` to call the `basic_ack` method on the channel instance.
|
||||
|
||||
Please see `examples/basic_consumer_threaded.py` for an example. As always, `SelectConnection` and a fully async consumer/publisher is the preferred method of using Pika.
|
||||
|
||||
Heartbeats are now sent at an interval equal to 1/2 of the negotiated idle connection timeout. RabbitMQ's default timeout value is 60 seconds, so heartbeats will be sent at a 30 second interval. In addition, Pika's check for an idle connection will be done at an interval equal to the timeout value plus 5 seconds to allow for delays. This results in an interval of 65 seconds by default.
|
||||
|
||||
0.11.2 2017-11-30
|
||||
-----------------
|
||||
|
||||
`GitHub milestone <https://github.com/pika/pika/milestone/11>`_
|
||||
|
||||
`0.11.2 <https://github.com/pika/pika/compare/0.11.1...0.11.2>`_
|
||||
|
||||
- Remove `+` character from platform releases string (`PR <https://github.com/pika/pika/pull/895>`_)
|
||||
|
||||
0.11.1 2017-11-27
|
||||
-----------------
|
||||
|
||||
`GitHub milestone <https://github.com/pika/pika/milestone/10>`_
|
||||
|
||||
`0.11.1 <https://github.com/pika/pika/compare/0.11.0...0.11.1>`_
|
||||
|
||||
- Fix `BlockingConnection` to ensure event loop exits (`PR <https://github.com/pika/pika/pull/887>`_)
|
||||
- Heartbeat timeouts will use the client value if specified (`PR <https://github.com/pika/pika/pull/874>`_)
|
||||
- Allow setting some common TCP options (`PR <https://github.com/pika/pika/pull/880>`_)
|
||||
- Errors when decoding Unicode are ignored (`PR <https://github.com/pika/pika/pull/890>`_)
|
||||
- Fix large number encoding (`PR <https://github.com/pika/pika/pull/888>`_)
|
||||
|
||||
0.11.0 2017-07-29
|
||||
-----------------
|
||||
|
||||
`GitHub milestone <https://github.com/pika/pika/milestone/9>`_
|
||||
|
||||
`0.11.0 <https://github.com/pika/pika/compare/0.10.0...0.11.0>`_
|
||||
|
||||
- Simplify Travis CI configuration for OS X.
|
||||
- Add `asyncio` connection adapter for Python 3.4 and newer.
|
||||
- Connection failures that occur after the socket is opened and before the
|
||||
AMQP connection is ready to go are now reported by calling the connection
|
||||
error callback. Previously these were not consistently reported.
|
||||
- In BaseConnection.close, call _handle_ioloop_stop only if the connection is
|
||||
already closed to allow the asynchronous close operation to complete
|
||||
gracefully.
|
||||
- Pass error information from failed socket connection to user callbacks
|
||||
on_open_error_callback and on_close_callback with result_code=-1.
|
||||
- ValueError is raised when a completion callback is passed to an asynchronous
|
||||
(nowait) Channel operation. It's an application error to pass a non-None
|
||||
completion callback with an asynchronous request, because this callback can
|
||||
never be serviced in the asynchronous scenario.
|
||||
- `Channel.basic_reject` fixed to allow `delivery_tag` to be of type `long`
|
||||
as well as `int`. (by quantum5)
|
||||
- Implemented support for blocked connection timeouts in
|
||||
`pika.connection.Connection`. This feature is available to all pika adapters.
|
||||
See `pika.connection.ConnectionParameters` docstring to learn more about
|
||||
`blocked_connection_timeout` configuration.
|
||||
- Deprecated the `heartbeat_interval` arg in `pika.ConnectionParameters` in
|
||||
favor of the `heartbeat` arg for consistency with the other connection
|
||||
parameters classes `pika.connection.Parameters` and `pika.URLParameters`.
|
||||
- When the `port` arg is not set explicitly in `ConnectionParameters`
|
||||
constructor, but the `ssl` arg is set explicitly, then set the port value to
|
||||
to the default AMQP SSL port if SSL is enabled, otherwise to the default
|
||||
AMQP plaintext port.
|
||||
- `URLParameters` will raise ValueError if a non-empty URL scheme other than
|
||||
{amqp | amqps | http | https} is specified.
|
||||
- `InvalidMinimumFrameSize` and `InvalidMaximumFrameSize` exceptions are
|
||||
deprecated. pika.connection.Parameters.frame_max property setter now raises
|
||||
the standard `ValueError` exception when the value is out of bounds.
|
||||
- Removed deprecated parameter `type` in `Channel.exchange_declare` and
|
||||
`BlockingChannel.exchange_declare` in favor of the `exchange_type` arg that
|
||||
doesn't overshadow the builtin `type` keyword.
|
||||
- Channel.close() on OPENING channel transitions it to CLOSING instead of
|
||||
raising ChannelClosed.
|
||||
- Channel.close() on CLOSING channel raises `ChannelAlreadyClosing`; used to
|
||||
raise `ChannelClosed`.
|
||||
- Connection.channel() raises `ConnectionClosed` if connection is not in OPEN
|
||||
state.
|
||||
- When performing graceful close on a channel and `Channel.Close` from broker
|
||||
arrives while waiting for CloseOk, don't release the channel number until
|
||||
CloseOk arrives to avoid race condition that may lead to a new channel
|
||||
receiving the CloseOk that was destined for the closing channel.
|
||||
- The `backpressure_detection` option of `ConnectionParameters` and
|
||||
`URLParameters` property is DEPRECATED in favor of `Connection.Blocked` and
|
||||
`Connection.Unblocked`. See `Connection.add_on_connection_blocked_callback`.
|
||||
|
||||
0.10.0 2015-09-02
|
||||
-----------------
|
||||
|
||||
`0.10.0 <https://github.com/pika/pika/compare/0.9.14...0.10.0>`_
|
||||
|
||||
- a9bf96d - LibevConnection: Fixed dict chgd size during iteration (Michael Laing)
|
||||
- 388c55d - SelectConnection: Fixed KeyError exceptions in IOLoop timeout executions (Shinji Suzuki)
|
||||
- 4780de3 - BlockingConnection: Add support to make BlockingConnection a Context Manager (@reddec)
|
||||
|
||||
0.10.0b2 2015-07-15
|
||||
-------------------
|
||||
|
||||
- f72b58f - Fixed failure to purge _ConsumerCancellationEvt from BlockingChannel._pending_events during basic_cancel. (Vitaly Kruglikov)
|
||||
|
||||
0.10.0b1 2015-07-10
|
||||
-------------------
|
||||
|
||||
High-level summary of notable changes:
|
||||
|
||||
- Change to 3-Clause BSD License
|
||||
- Python 3.x support
|
||||
- Over 150 commits from 19 contributors
|
||||
- Refactoring of SelectConnection ioloop
|
||||
- This major release contains certain non-backward-compatible API changes as
|
||||
well as significant performance improvements in the `BlockingConnection`
|
||||
adapter.
|
||||
- Non-backward-compatible changes in `Channel.add_on_return_callback` callback's
|
||||
signature.
|
||||
- The `AsyncoreConnection` adapter was retired
|
||||
|
||||
**Details**
|
||||
|
||||
Python 3.x: this release introduces python 3.x support. Tested on Python 3.3
|
||||
and 3.4.
|
||||
|
||||
`AsyncoreConnection`: Retired this legacy adapter to reduce maintenance burden;
|
||||
the recommended replacement is the `SelectConnection` adapter.
|
||||
|
||||
`SelectConnection`: ioloop was refactored for compatibility with other ioloops.
|
||||
|
||||
`Channel.add_on_return_callback`: The callback is now passed the individual
|
||||
parameters channel, method, properties, and body instead of a tuple of those
|
||||
values for congruence with other similar callbacks.
|
||||
|
||||
`BlockingConnection`: This adapter underwent a makeover under the hood and
|
||||
gained significant performance improvements as well as enhanced timer
|
||||
resolution. It is now implemented as a client of the `SelectConnection` adapter.
|
||||
|
||||
Below is an overview of the `BlockingConnection` and `BlockingChannel` API
|
||||
changes:
|
||||
|
||||
- Recursion: the new implementation eliminates callback recursion that
|
||||
sometimes blew out the stack in the legacy implementation (e.g.,
|
||||
publish -> consumer_callback -> publish -> consumer_callback, etc.). While
|
||||
`BlockingConnection.process_data_events` and `BlockingConnection.sleep` may
|
||||
still be called from the scope of the blocking adapter's callbacks in order
|
||||
to process pending I/O, additional callbacks will be suppressed whenever
|
||||
`BlockingConnection.process_data_events` and `BlockingConnection.sleep` are
|
||||
nested in any combination; in that case, the callback information will be
|
||||
bufferred and dispatched once nesting unwinds and control returns to the
|
||||
level-zero dispatcher.
|
||||
- `BlockingConnection.connect`: this method was removed in favor of the
|
||||
constructor as the only way to establish connections; this reduces
|
||||
maintenance burden, while improving reliability of the adapter.
|
||||
- `BlockingConnection.process_data_events`: added the optional parameter
|
||||
`time_limit`.
|
||||
- `BlockingConnection.add_on_close_callback`: removed; legacy raised
|
||||
`NotImplementedError`.
|
||||
- `BlockingConnection.add_on_open_callback`: removed; legacy raised
|
||||
`NotImplementedError`.
|
||||
- `BlockingConnection.add_on_open_error_callback`: removed; legacy raised
|
||||
`NotImplementedError`.
|
||||
- `BlockingConnection.add_backpressure_callback`: not supported
|
||||
- `BlockingConnection.set_backpressure_multiplier`: not supported
|
||||
- `BlockingChannel.add_on_flow_callback`: not supported; per docstring in
|
||||
channel.py: "Note that newer versions of RabbitMQ will not issue this but
|
||||
instead use TCP backpressure".
|
||||
- `BlockingChannel.flow`: not supported
|
||||
- `BlockingChannel.force_data_events`: removed as it is no longer necessary
|
||||
following redesign of the adapter.
|
||||
- Removed the `nowait` parameter from `BlockingChannel` methods, forcing
|
||||
`nowait=False` (former API default) in the implementation; this is more
|
||||
suitable for the blocking nature of the adapter and its error-reporting
|
||||
strategy; this concerns the following methods: `basic_cancel`,
|
||||
`confirm_delivery`, `exchange_bind`, `exchange_declare`, `exchange_delete`,
|
||||
`exchange_unbind`, `queue_bind`, `queue_declare`, `queue_delete`, and
|
||||
`queue_purge`.
|
||||
- `BlockingChannel.basic_cancel`: returns a sequence instead of None; for a
|
||||
`no_ack=True` consumer, `basic_cancel` returns a sequence of pending
|
||||
messages that arrived before broker confirmed the cancellation.
|
||||
- `BlockingChannel.consume`: added new optional kwargs `arguments` and
|
||||
`inactivity_timeout`. Also, raises ValueError if the consumer creation
|
||||
parameters don't match those used to create the existing queue consumer
|
||||
generator, if any; this happens when you break out of the consume loop, then
|
||||
call `BlockingChannel.consume` again with different consumer-creation args
|
||||
without first cancelling the previous queue consumer generator via
|
||||
`BlockingChannel.cancel`. The legacy implementation would silently resume
|
||||
consuming from the existing queue consumer generator even if the subsequent
|
||||
`BlockingChannel.consume` was invoked with a different queue name, etc.
|
||||
- `BlockingChannel.cancel`: returns 0; the legacy implementation tried to
|
||||
return the number of requeued messages, but this number was not accurate
|
||||
as it didn't include the messages returned by the Channel class; this count
|
||||
is not generally useful, so returning 0 is a reasonable replacement.
|
||||
- `BlockingChannel.open`: removed in favor of having a single mechanism for
|
||||
creating a channel (`BlockingConnection.channel`); this reduces maintenance
|
||||
burden, while improving reliability of the adapter.
|
||||
- `BlockingChannel.confirm_delivery`: raises UnroutableError when unroutable
|
||||
messages that were sent prior to this call are returned before we receive
|
||||
Confirm.Select-ok.
|
||||
- `BlockingChannel.basic_publish: always returns True when delivery
|
||||
confirmation is not enabled (publisher-acks = off); the legacy implementation
|
||||
returned a bool in this case if `mandatory=True` to indicate whether the
|
||||
message was delivered; however, this was non-deterministic, because
|
||||
Basic.Return is asynchronous and there is no way to know how long to wait
|
||||
for it or its absence. The legacy implementation returned None when
|
||||
publishing with publisher-acks = off and `mandatory=False`. The new
|
||||
implementation always returns True when publishing while
|
||||
publisher-acks = off.
|
||||
- `BlockingChannel.publish`: a new alternate method (vs. `basic_publish`) for
|
||||
publishing a message with more detailed error reporting via UnroutableError
|
||||
and NackError exceptions.
|
||||
- `BlockingChannel.start_consuming`: raises pika.exceptions.RecursionError if
|
||||
called from the scope of a `BlockingConnection` or `BlockingChannel`
|
||||
callback.
|
||||
- `BlockingChannel.get_waiting_message_count`: new method; returns the number
|
||||
of messages that may be retrieved from the current queue consumer generator
|
||||
via `BasicChannel.consume` without blocking.
|
||||
|
||||
**Commits**
|
||||
|
||||
- 5aaa753 - Fixed SSL import and removed no_ack=True in favor of explicit AMQP message handling based on deferreds (skftn)
|
||||
- 7f222c2 - Add checkignore for codeclimate (Gavin M. Roy)
|
||||
- 4dec370 - Implemented BlockingChannel.flow; Implemented BlockingConnection.add_on_connection_blocked_callback; Implemented BlockingConnection.add_on_connection_unblocked_callback. (Vitaly Kruglikov)
|
||||
- 4804200 - Implemented blocking adapter acceptance test for exchange-to-exchange binding. Added rudimentary validation of BasicProperties passthru in blocking adapter publish tests. Updated CHANGELOG. (Vitaly Kruglikov)
|
||||
- 4ec07fd - Fixed sending of data in TwistedProtocolConnection (Vitaly Kruglikov)
|
||||
- a747fb3 - Remove my copyright from forward_server.py test utility. (Vitaly Kruglikov)
|
||||
- 94246d2 - Return True from basic_publish when pubacks is off. Implemented more blocking adapter accceptance tests. (Vitaly Kruglikov)
|
||||
- 3ce013d - PIKA-609 Wait for broker to dispatch all messages to client before cancelling consumer in TestBasicCancelWithNonAckableConsumer and TestBasicCancelWithAckableConsumer (Vitaly Kruglikov)
|
||||
- 293f778 - Created CHANGELOG entry for release 0.10.0. Fixed up callback documentation for basic_get, basic_consume, and add_on_return_callback. (Vitaly Kruglikov)
|
||||
- 16d360a - Removed the legacy AsyncoreConnection adapter in favor of the recommended SelectConnection adapter. (Vitaly Kruglikov)
|
||||
- 240a82c - Defer creation of poller's event loop interrupt socket pair until start is called, because some SelectConnection users (e.g., BlockingConnection adapter) don't use the event loop, and these sockets would just get reported as resource leaks. (Vitaly Kruglikov)
|
||||
- aed5cae - Added EINTR loops in select_connection pollers. Addressed some pylint findings, including an error or two. Wrap socket.send and socket.recv calls in EINTR loops Use the correct exception for socket.error and select.error and get errno depending on python version. (Vitaly Kruglikov)
|
||||
- 498f1be - Allow passing exchange, queue and routing_key as text, handle short strings as text in python3 (saarni)
|
||||
- 9f7f243 - Restored basic_consume, basic_cancel, and add_on_cancel_callback (Vitaly Kruglikov)
|
||||
- 18c9909 - Reintroduced BlockingConnection.process_data_events. (Vitaly Kruglikov)
|
||||
- 4b25cb6 - Fixed BlockingConnection/BlockingChannel acceptance and unit tests (Vitaly Kruglikov)
|
||||
- bfa932f - Facilitate proper connection state after BasicConnection._adapter_disconnect (Vitaly Kruglikov)
|
||||
- 9a09268 - Fixed BlockingConnection test that was failing with ConnectionClosed error. (Vitaly Kruglikov)
|
||||
- 5a36934 - Copied synchronous_connection.py from pika-synchronous branch Fixed pylint findings Integrated SynchronousConnection with the new ioloop in SelectConnection Defined dedicated message classes PolledMessage and ConsumerMessage and moved from BlockingChannel to module-global scope. Got rid of nowait args from BlockingChannel public API methods Signal unroutable messages via UnroutableError exception. Signal Nack'ed messages via NackError exception. These expose more information about the failure than legacy basic_publich API. Removed set_timeout and backpressure callback methods Restored legacy `is_open`, etc. property names (Vitaly Kruglikov)
|
||||
- 6226dc0 - Remove deprecated --use-mirrors (Gavin M. Roy)
|
||||
- 1a7112f - Raise ConnectionClosed when sending a frame with no connection (#439) (Gavin M. Roy)
|
||||
- 9040a14 - Make delivery_tag non-optional (#498) (Gavin M. Roy)
|
||||
- 86aabc2 - Bump version (Gavin M. Roy)
|
||||
- 562075a - Update a few testing things (Gavin M. Roy)
|
||||
- 4954d38 - use unicode_type in blocking_connection.py (Antti Haapala)
|
||||
- 133d6bc - Let Travis install ordereddict for Python 2.6, and ttest 3.3, 3.4 too. (Antti Haapala)
|
||||
- 0d2287d - Pika Python 3 support (Antti Haapala)
|
||||
- 3125c79 - SSLWantRead is not supported before python 2.7.9 and 3.3 (Will)
|
||||
- 9a9c46c - Fixed TestDisconnectDuringConnectionStart: it turns out that depending on callback order, it might get either ProbableAuthenticationError or ProbableAccessDeniedError. (Vitaly Kruglikov)
|
||||
- cd8c9b0 - A fix the write starvation problem that we see with tornado and pika (Will)
|
||||
- 8654fbc - SelectConnection - make interrupt socketpair non-blocking (Will)
|
||||
- 4f3666d - Added copyright in forward_server.py and fixed NameError bug (Vitaly Kruglikov)
|
||||
- f8ebbbc - ignore docs (Gavin M. Roy)
|
||||
- a344f78 - Updated codeclimate config (Gavin M. Roy)
|
||||
- 373c970 - Try and fix pathing issues in codeclimate (Gavin M. Roy)
|
||||
- 228340d - Ignore codegen (Gavin M. Roy)
|
||||
- 4db0740 - Add a codeclimate config (Gavin M. Roy)
|
||||
- 7e989f9 - Slight code re-org, usage comment and better naming of test file. (Will)
|
||||
- 287be36 - Set up _kqueue member of KQueuePoller before calling super constructor to avoid exception due to missing _kqueue member. Call `self._map_event(event)` instead of `self._map_event(event.filter)`, because `KQueuePoller._map_event()` assumes it's getting an event, not an event filter. (Vitaly Kruglikov)
|
||||
- 62810fb - Fix issue #412: reset BlockingConnection._read_poller in BlockingConnection._adapter_disconnect() to guard against accidental access to old file descriptor. (Vitaly Kruglikov)
|
||||
- 03400ce - Rationalise adapter acceptance tests (Will)
|
||||
- 9414153 - Fix bug selecting non epoll poller (Will)
|
||||
- 4f063df - Use user heartbeat setting if server proposes none (Pau Gargallo)
|
||||
- 9d04d6e - Deactivate heartbeats when heartbeat_interval is 0 (Pau Gargallo)
|
||||
- a52a608 - Bug fix and review comments. (Will)
|
||||
- e3ebb6f - Fix incorrect x-expires argument in acceptance tests (Will)
|
||||
- 294904e - Get BlockingConnection into consistent state upon loss of TCP/IP connection with broker and implement acceptance tests for those cases. (Vitaly Kruglikov)
|
||||
- 7f91a68 - Make SelectConnection behave like an ioloop (Will)
|
||||
- dc9db2b - Perhaps 5 seconds is too agressive for travis (Gavin M. Roy)
|
||||
- c23e532 - Lower the stuck test timeout (Gavin M. Roy)
|
||||
- 1053ebc - Late night bug (Gavin M. Roy)
|
||||
- cd6c1bf - More BaseConnection._handle_error cleanup (Gavin M. Roy)
|
||||
- a0ff21c - Fix the test to work with Python 2.6 (Gavin M. Roy)
|
||||
- 748e8aa - Remove pypy for now (Gavin M. Roy)
|
||||
- 1c921c1 - Socket close/shutdown cleanup (Gavin M. Roy)
|
||||
- 5289125 - Formatting update from PR (Gavin M. Roy)
|
||||
- d235989 - Be more specific when calling getaddrinfo (Gavin M. Roy)
|
||||
- b5d1b31 - Reflect the method name change in pika.callback (Gavin M. Roy)
|
||||
- df7d3b7 - Cleanup BlockingConnection in a few places (Gavin M. Roy)
|
||||
- cd99e1c - Rename method due to use in BlockingConnection (Gavin M. Roy)
|
||||
- 7e0d1b3 - Use google style with yapf instead of pep8 (Gavin M. Roy)
|
||||
- 7dc9bab - Refactor socket writing to not use sendall #481 (Gavin M. Roy)
|
||||
- 4838789 - Dont log the fd #521 (Gavin M. Roy)
|
||||
- 765107d - Add Connection.Blocked callback registration methods #476 (Gavin M. Roy)
|
||||
- c15b5c1 - Fix _blocking typo pointed out in #513 (Gavin M. Roy)
|
||||
- 759ac2c - yapf of codegen (Gavin M. Roy)
|
||||
- 9dadd77 - yapf cleanup of codegen and spec (Gavin M. Roy)
|
||||
- ddba7ce - Do not reject consumers with no_ack=True #486 #530 (Gavin M. Roy)
|
||||
- 4528a1a - yapf reformatting of tests (Gavin M. Roy)
|
||||
- e7b6d73 - Remove catching AttributError (#531) (Gavin M. Roy)
|
||||
- 41ea5ea - Update README badges [skip ci] (Gavin M. Roy)
|
||||
- 6af987b - Add note on contributing (Gavin M. Roy)
|
||||
- 161fc0d - yapf formatting cleanup (Gavin M. Roy)
|
||||
- edcb619 - Add PYPY to travis testing (Gavin M. Roy)
|
||||
- 2225771 - Change the coverage badge (Gavin M. Roy)
|
||||
- 8f7d451 - Move to codecov from coveralls (Gavin M. Roy)
|
||||
- b80407e - Add confirm_delivery to example (Andrew Smith)
|
||||
- 6637212 - Update base_connection.py (bstemshorn)
|
||||
- 1583537 - #544 get_waiting_message_count() (markcf)
|
||||
- 0c9be99 - Fix #535: pass expected reply_code and reply_text from method frame to Connection._on_disconnect from Connection._on_connection_closed (Vitaly Kruglikov)
|
||||
- d11e73f - Propagate ConnectionClosed exception out of BlockingChannel._send_method() and log ConnectionClosed in BlockingConnection._on_connection_closed() (Vitaly Kruglikov)
|
||||
- 63d2951 - Fix #541 - make sure connection state is properly reset when BlockingConnection._check_state_on_disconnect raises ConnectionClosed. This supplements the previously-merged PR #450 by getting the connection into consistent state. (Vitaly Kruglikov)
|
||||
- 71bc0eb - Remove unused self.fd attribute from BaseConnection (Vitaly Kruglikov)
|
||||
- 8c08f93 - PIKA-532 Removed unnecessary params (Vitaly Kruglikov)
|
||||
- 6052ecf - PIKA-532 Fix bug in BlockingConnection._handle_timeout that was preventing _on_connection_closed from being called when not closing. (Vitaly Kruglikov)
|
||||
- 562aa15 - pika: callback: Display exception message when callback fails. (Stuart Longland)
|
||||
- 452995c - Typo fix in connection.py (Andrew)
|
||||
- 361c0ad - Added some missing yields (Robert Weidlich)
|
||||
- 0ab5a60 - Added complete example for python twisted service (Robert Weidlich)
|
||||
- 4429110 - Add deployment and webhooks (Gavin M. Roy)
|
||||
- 7e50302 - Fix has_content style in codegen (Andrew Grigorev)
|
||||
- 28c2214 - Fix the trove categorization (Gavin M. Roy)
|
||||
- de8b545 - Ensure frames can not be interspersed on send (Gavin M. Roy)
|
||||
- 8fe6bdd - Fix heartbeat behaviour after connection failure. (Kyösti Herrala)
|
||||
- c123472 - Updating BlockingChannel.basic_get doc (it does not receive a callback like the rest of the adapters) (Roberto Decurnex)
|
||||
- b5f52fb - Fix number of arguments passed to _on_return callback (Axel Eirola)
|
||||
- 765139e - Lower default TIMEOUT to 0.01 (bra-fsn)
|
||||
- 6cc22a5 - Fix confirmation on reconnects (bra-fsn)
|
||||
- f4faf0a - asynchronous publisher and subscriber examples refactored to follow the StepDown rule (Riccardo Cirimelli)
|
||||
|
||||
0.9.14 - 2014-07-11
|
||||
-------------------
|
||||
|
||||
`0.9.14 <https://github.com/pika/pika/compare/0.9.13...0.9.14>`_
|
||||
|
||||
- 57fe43e - fix test to generate a correct range of random ints (ml)
|
||||
- 0d68dee - fix async watcher for libev_connection (ml)
|
||||
- 01710ad - Use default username and password if not specified in URLParameters (Sean Dwyer)
|
||||
- fae328e - documentation typo (Jeff Fein-Worton)
|
||||
- afbc9e0 - libev_connection: reset_io_watcher (ml)
|
||||
- 24332a2 - Fix the manifest (Gavin M. Roy)
|
||||
- acdfdef - Remove useless test (Gavin M. Roy)
|
||||
- 7918e1a - Skip libev tests if pyev is not installed or if they are being run in pypy (Gavin M. Roy)
|
||||
- bb583bf - Remove the deprecated test (Gavin M. Roy)
|
||||
- aecf3f2 - Don't reject a message if the channel is not open (Gavin M. Roy)
|
||||
- e37f336 - Remove UTF-8 decoding in spec (Gavin M. Roy)
|
||||
- ddc35a9 - Update the unittest to reflect removal of force binary (Gavin M. Roy)
|
||||
- fea2476 - PEP8 cleanup (Gavin M. Roy)
|
||||
- 9b97956 - Remove force_binary (Gavin M. Roy)
|
||||
- a42dd90 - Whitespace required (Gavin M. Roy)
|
||||
- 85867ea - Update the content_frame_dispatcher tests to reflect removal of auto-cast utf-8 (Gavin M. Roy)
|
||||
- 5a4bd5d - Remove unicode casting (Gavin M. Roy)
|
||||
- efea53d - Remove force binary and unicode casting (Gavin M. Roy)
|
||||
- e918d15 - Add methods to remove deprecation warnings from asyncore (Gavin M. Roy)
|
||||
- 117f62d - Add a coveragerc to ignore the auto generated pika.spec (Gavin M. Roy)
|
||||
- 52f4485 - Remove pypy tests from travis for now (Gavin M. Roy)
|
||||
- c3aa958 - Update README.rst (Gavin M. Roy)
|
||||
- 3e2319f - Delete README.md (Gavin M. Roy)
|
||||
- c12b0f1 - Move to RST (Gavin M. Roy)
|
||||
- 704f5be - Badging updates (Gavin M. Roy)
|
||||
- 7ae33ca - Update for coverage info (Gavin M. Roy)
|
||||
- ae7ca86 - add libev_adapter_tests.py; modify .travis.yml to install libev and pyev (ml)
|
||||
- f86aba5 - libev_connection: add **kwargs to _handle_event; suppress default_ioloop reuse warning (ml)
|
||||
- 603f1cf - async_test_base: add necessary args to _on_cconn_closed (ml)
|
||||
- 3422007 - add libev_adapter_tests.py (ml)
|
||||
- 6cbab0c - removed relative imports and importing urlparse from urllib.parse for py3+ (a-tal)
|
||||
- f808464 - libev_connection: add async watcher; add optional parameters to add_timeout (ml)
|
||||
- c041c80 - Remove ev all together for now (Gavin M. Roy)
|
||||
- 9408388 - Update the test descriptions and timeout (Gavin M. Roy)
|
||||
- 1b552e0 - Increase timeout (Gavin M. Roy)
|
||||
- 69a1f46 - Remove the pyev requirement for 2.6 testing (Gavin M. Roy)
|
||||
- fe062d2 - Update package name (Gavin M. Roy)
|
||||
- 611ad0e - Distribute the LICENSE and README.md (#350) (Gavin M. Roy)
|
||||
- df5e1d8 - Ensure that the entire frame is written using socket.sendall (#349) (Gavin M. Roy)
|
||||
- 69ec8cf - Move the libev install to before_install (Gavin M. Roy)
|
||||
- a75f693 - Update test structure (Gavin M. Roy)
|
||||
- 636b424 - Update things to ignore (Gavin M. Roy)
|
||||
- b538c68 - Add tox, nose.cfg, update testing config (Gavin M. Roy)
|
||||
- a0e7063 - add some tests to increase coverage of pika.connection (Charles Law)
|
||||
- c76d9eb - Address issue #459 (Gavin M. Roy)
|
||||
- 86ad2db - Raise exception if positional arg for parameters isn't an instance of Parameters (Gavin M. Roy)
|
||||
- 14d08e1 - Fix for python 2.6 (Gavin M. Roy)
|
||||
- bd388a3 - Use the first unused channel number addressing #404, #460 (Gavin M. Roy)
|
||||
- e7676e6 - removing a debug that was left in last commit (James Mutton)
|
||||
- 6c93b38 - Fixing connection-closed behavior to detect on attempt to publish (James Mutton)
|
||||
- c3f0356 - Initialize bytes_written in _handle_write() (Jonathan Kirsch)
|
||||
- 4510e95 - Fix _handle_write() may not send full frame (Jonathan Kirsch)
|
||||
- 12b793f - fixed Tornado Consumer example to successfully reconnect (Yang Yang)
|
||||
- f074444 - remove forgotten import of ordereddict (Pedro Abranches)
|
||||
- 1ba0aea - fix last merge (Pedro Abranches)
|
||||
- 10490a6 - change timeouts structure to list to maintain scheduling order (Pedro Abranches)
|
||||
- 7958394 - save timeouts in ordered dict instead of dict (Pedro Abranches)
|
||||
- d2746bf - URLParameters and ConnectionParameters accept unicode strings (Allard Hoeve)
|
||||
- 596d145 - previous fix for AttributeError made parent and child class methods identical, remove duplication (James Mutton)
|
||||
- 42940dd - UrlParameters Docs: fixed amqps scheme examples (Riccardo Cirimelli)
|
||||
- 43904ff - Dont test this in PyPy due to sort order issue (Gavin M. Roy)
|
||||
- d7d293e - Don't leave __repr__ sorting up to chance (Gavin M. Roy)
|
||||
- 848c594 - Add integration test to travis and fix invocation (Gavin M. Roy)
|
||||
- 2678275 - Add pypy to travis tests (Gavin M. Roy)
|
||||
- 1877f3d - Also addresses issue #419 (Gavin M. Roy)
|
||||
- 470c245 - Address issue #419 (Gavin M. Roy)
|
||||
- ca3cb59 - Address issue #432 (Gavin M. Roy)
|
||||
- a3ff6f2 - Default frame max should be AMQP FRAME_MAX (Gavin M. Roy)
|
||||
- ff3d5cb - Remove max consumer tag test due to change in code. (Gavin M. Roy)
|
||||
- 6045dda - Catch KeyError (#437) to ensure that an exception is not raised in a race condition (Gavin M. Roy)
|
||||
- 0b4d53a - Address issue #441 (Gavin M. Roy)
|
||||
- 180e7c4 - Update license and related files (Gavin M. Roy)
|
||||
- 256ed3d - Added Jython support. (Erik Olof Gunnar Andersson)
|
||||
- f73c141 - experimental work around for recursion issue. (Erik Olof Gunnar Andersson)
|
||||
- a623f69 - Prevent #436 by iterating the keys and not the dict (Gavin M. Roy)
|
||||
- 755fcae - Add support for authentication_failure_close, connection.blocked (Gavin M. Roy)
|
||||
- c121243 - merge upstream master (Michael Laing)
|
||||
- a08dc0d - add arg to channel.basic_consume (Pedro Abranches)
|
||||
- 10b136d - Documentation fix (Anton Ryzhov)
|
||||
- 9313307 - Fixed minor markup errors. (Jorge Puente Sarrín)
|
||||
- fb3e3cf - Fix the spelling of UnsupportedAMQPFieldException (Garrett Cooper)
|
||||
- 03d5da3 - connection.py: Propagate the force_channel keyword parameter to methods involved in channel creation (Michael Laing)
|
||||
- 7bbcff5 - Documentation fix for basic_publish (JuhaS)
|
||||
- 01dcea7 - Expose no_ack and exclusive to BlockingChannel.consume (Jeff Tang)
|
||||
- d39b6aa - Fix BlockingChannel.basic_consume does not block on non-empty queues (Juhyeong Park)
|
||||
- 6e1d295 - fix for issue 391 and issue 307 (Qi Fan)
|
||||
- d9ffce9 - Update parameters.rst (cacovsky)
|
||||
- 6afa41e - Add additional badges (Gavin M. Roy)
|
||||
- a255925 - Fix return value on dns resolution issue (Laurent Eschenauer)
|
||||
- 3f7466c - libev_connection: tweak docs (Michael Laing)
|
||||
- 0aaed93 - libev_connection: Fix varable naming (Michael Laing)
|
||||
- 0562d08 - libev_connection: Fix globals warning (Michael Laing)
|
||||
- 22ada59 - libev_connection: use globals to track sigint and sigterm watchers as they are created globally within libev (Michael Laing)
|
||||
- 2649b31 - Move badge [skip ci] (Gavin M. Roy)
|
||||
- f70eea1 - Remove pypy and installation attempt of pyev (Gavin M. Roy)
|
||||
- f32e522 - Conditionally skip external connection adapters if lib is not installed (Gavin M. Roy)
|
||||
- cce97c5 - Only install pyev on python 2.7 (Gavin M. Roy)
|
||||
- ff84462 - Add travis ci support (Gavin M. Roy)
|
||||
- cf971da - lib_evconnection: improve signal handling; add callback (Michael Laing)
|
||||
- 9adb269 - bugfix in returning a list in Py3k (Alex Chandel)
|
||||
- c41d5b9 - update exception syntax for Py3k (Alex Chandel)
|
||||
- c8506f1 - fix _adapter_connect (Michael Laing)
|
||||
- 67cb660 - Add LibevConnection to README (Michael Laing)
|
||||
- 1f9e72b - Propagate low-level connection errors to the AMQPConnectionError. (Bjorn Sandberg)
|
||||
- e1da447 - Avoid race condition in _on_getok on successive basic_get() when clearing out callbacks (Jeff)
|
||||
- 7a09979 - Add support for upcoming Connection.Blocked/Unblocked (Gavin M. Roy)
|
||||
- 53cce88 - TwistedChannel correctly handles multi-argument deferreds. (eivanov)
|
||||
- 66f8ace - Use uuid when creating unique consumer tag (Perttu Ranta-aho)
|
||||
- 4ee2738 - Limit the growth of Channel._cancelled, use deque instead of list. (Perttu Ranta-aho)
|
||||
- 0369aed - fix adapter references and tweak docs (Michael Laing)
|
||||
- 1738c23 - retry select.select() on EINTR (Cenk Alti)
|
||||
- 1e55357 - libev_connection: reset internal state on reconnect (Michael Laing)
|
||||
- 708559e - libev adapter (Michael Laing)
|
||||
- a6b7c8b - Prioritize EPollPoller and KQueuePoller over PollPoller and SelectPoller (Anton Ryzhov)
|
||||
- 53400d3 - Handle socket errors in PollPoller and EPollPoller Correctly check 'select.poll' availability (Anton Ryzhov)
|
||||
- a6dc969 - Use dict.keys & items instead of iterkeys & iteritems (Alex Chandel)
|
||||
- 5c1b0d0 - Use print function syntax, in examples (Alex Chandel)
|
||||
- ac9f87a - Fixed a typo in the name of the Asyncore Connection adapter (Guruprasad)
|
||||
- dfbba50 - Fixed bug mentioned in Issue #357 (Erik Andersson)
|
||||
- c906a2d - Drop additional flags when getting info for the hostnames, log errors (#352) (Gavin M. Roy)
|
||||
- baf23dd - retry poll() on EINTR (Cenk Alti)
|
||||
- 7cd8762 - Address ticket #352 catching an error when socket.getprotobyname fails (Gavin M. Roy)
|
||||
- 6c3ec75 - Prep for 0.9.14 (Gavin M. Roy)
|
||||
- dae7a99 - Bump to 0.9.14p0 (Gavin M. Roy)
|
||||
- 620edc7 - Use default port and virtual host if omitted in URLParameters (Issue #342) (Gavin M. Roy)
|
||||
- 42a8787 - Move the exception handling inside the while loop (Gavin M. Roy)
|
||||
- 10e0264 - Fix connection back pressure detection issue #347 (Gavin M. Roy)
|
||||
- 0bfd670 - Fixed mistake in commit 3a19d65. (Erik Andersson)
|
||||
- da04bc0 - Fixed Unknown state on disconnect error message generated when closing connections. (Erik Andersson)
|
||||
- 3a19d65 - Alternative solution to fix #345. (Erik Andersson)
|
||||
- abf9fa8 - switch to sendall to send entire frame (Dustin Koupal)
|
||||
- 9ce8ce4 - Fixed the async publisher example to work with reconnections (Raphaël De Giusti)
|
||||
- 511028a - Fix typo in TwistedChannel docstring (cacovsky)
|
||||
- 8b69e5a - calls self._adapter_disconnect() instead of self.disconnect() which doesn't actually exist #294 (Mark Unsworth)
|
||||
- 06a5cf8 - add NullHandler to prevent logging warnings (Cenk Alti)
|
||||
- f404a9a - Fix #337 cannot start ioloop after stop (Ralf Nyren)
|
||||
|
||||
0.9.13 - 2013-05-15
|
||||
-------------------
|
||||
|
||||
`0.9.13 <https://github.com/pika/pika/compare/0.9.12...0.9.13>`_
|
||||
|
||||
**Major Changes**
|
||||
|
||||
- IPv6 Support with thanks to Alessandro Tagliapietra for initial prototype
|
||||
- Officially remove support for <= Python 2.5 even though it was broken already
|
||||
- Drop pika.simplebuffer.SimpleBuffer in favor of the Python stdlib collections.deque object
|
||||
- New default object for receiving content is a "bytes" object which is a str wrapper in Python 2, but paves way for Python 3 support
|
||||
- New "Raw" mode for frame decoding content frames (#334) addresses issues #331, #229 added by Garth Williamson
|
||||
- Connection and Disconnection logic refactored, allowing for cleaner separation of protocol logic and socket handling logic as well as connection state management
|
||||
- New "on_open_error_callback" argument in creating connection objects and new Connection.add_on_open_error_callback method
|
||||
- New Connection.connect method to cleanly allow for reconnection code
|
||||
- Support for all AMQP field types, using protocol specified signed/unsigned unpacking
|
||||
|
||||
**Backwards Incompatible Changes**
|
||||
|
||||
- Method signature for creating connection objects has new argument "on_open_error_callback" which is positionally before "on_close_callback"
|
||||
- Internal callback variable names in connection.Connection have been renamed and constants used. If you relied on any of these callbacks outside of their internal use, make sure to check out the new constants.
|
||||
- Connection._connect method, which was an internal only method is now deprecated and will raise a DeprecationWarning. If you relied on this method, your code needs to change.
|
||||
- pika.simplebuffer has been removed
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- BlockingConnection consumer generator does not free buffer when exited (#328)
|
||||
- Unicode body payloads in the blocking adapter raises exception (#333)
|
||||
- Support "b" short-short-int AMQP data type (#318)
|
||||
- Docstring type fix in adapters/select_connection (#316) fix by Rikard Hultén
|
||||
- IPv6 not supported (#309)
|
||||
- Stop the HeartbeatChecker when connection is closed (#307)
|
||||
- Unittest fix for SelectConnection (#336) fix by Erik Andersson
|
||||
- Handle condition where no connection or socket exists but SelectConnection needs a timeout for retrying a connection (#322)
|
||||
- TwistedAdapter lagging behind BaseConnection changes (#321) fix by Jan Urbański
|
||||
|
||||
**Other**
|
||||
|
||||
- Refactored documentation
|
||||
- Added Twisted Adapter example (#314) by nolinksoft
|
||||
|
||||
0.9.12 - 2013-03-18
|
||||
-------------------
|
||||
|
||||
`0.9.12 <https://github.com/pika/pika/compare/0.9.11...0.9.12>`_
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- New timeout id hashing was not unique
|
||||
|
||||
0.9.11 - 2013-03-17
|
||||
-------------------
|
||||
|
||||
`0.9.11 <https://github.com/pika/pika/compare/0.9.10...0.9.11>`_
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Address inconsistent channel close callback documentation and add the signature
|
||||
change to the TwistedChannel class (#305)
|
||||
- Address a missed timeout related internal data structure name change
|
||||
introduced in the SelectConnection 0.9.10 release. Update all connection
|
||||
adapters to use same signature and docstring (#306).
|
||||
|
||||
0.9.10 - 2013-03-16
|
||||
-------------------
|
||||
|
||||
`0.9.10 <https://github.com/pika/pika/compare/0.9.9...0.9.10>`_
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix timeout in twisted adapter (Submitted by cellscape)
|
||||
- Fix blocking_connection poll timer resolution to milliseconds (Submitted by cellscape)
|
||||
- Fix channel._on_close() without a method frame (Submitted by Richard Boulton)
|
||||
- Addressed exception on close (Issue #279 - fix by patcpsc)
|
||||
- 'messages' not initialized in BlockingConnection.cancel() (Issue #289 - fix by Mik Kocikowski)
|
||||
- Make queue_unbind behave like queue_bind (Issue #277)
|
||||
- Address closing behavioral issues for connections and channels (Issue #275)
|
||||
- Pass a Method frame to Channel._on_close in Connection._on_disconnect (Submitted by Jan Urbański)
|
||||
- Fix channel closed callback signature in the Twisted adapter (Submitted by Jan Urbański)
|
||||
- Don't stop the IOLoop on connection close for in the Twisted adapter (Submitted by Jan Urbański)
|
||||
- Update the asynchronous examples to fix reconnecting and have it work
|
||||
- Warn if the socket was closed such as if RabbitMQ dies without a Close frame
|
||||
- Fix URLParameters ssl_options (Issue #296)
|
||||
- Add state to BlockingConnection addressing (Issue #301)
|
||||
- Encode unicode body content prior to publishing (Issue #282)
|
||||
- Fix an issue with unicode keys in BasicProperties headers key (Issue #280)
|
||||
- Change how timeout ids are generated (Issue #254)
|
||||
- Address post close state issues in Channel (Issue #302)
|
||||
|
||||
** Behavior changes **
|
||||
|
||||
- Change core connection communication behavior to prefer outbound writes over reads, addressing a recursion issue
|
||||
- Update connection on close callbacks, changing callback method signature
|
||||
- Update channel on close callbacks, changing callback method signature
|
||||
- Give more info in the ChannelClosed exception
|
||||
- Change the constructor signature for BlockingConnection, block open/close callbacks
|
||||
- Disable the use of add_on_open_callback/add_on_close_callback methods in BlockingConnection
|
||||
|
||||
|
||||
0.9.9 - 2013-01-29
|
||||
------------------
|
||||
|
||||
`0.9.9 <https://github.com/pika/pika/compare/0.9.8...0.9.9>`_
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Only remove the tornado_connection.TornadoConnection file descriptor from the IOLoop if it's still open (Issue #221)
|
||||
- Allow messages with no body (Issue #227)
|
||||
- Allow for empty routing keys (Issue #224)
|
||||
- Don't raise an exception when trying to send a frame to a closed connection (Issue #229)
|
||||
- Only send a Connection.CloseOk if the connection is still open. (Issue #236 - Fix by noleaf)
|
||||
- Fix timeout threshold in blocking connection - (Issue #232 - Fix by Adam Flynn)
|
||||
- Fix closing connection while a channel is still open (Issue #230 - Fix by Adam Flynn)
|
||||
- Fixed misleading warning and exception messages in BaseConnection (Issue #237 - Fix by Tristan Penman)
|
||||
- Pluralised and altered the wording of the AMQPConnectionError exception (Issue #237 - Fix by Tristan Penman)
|
||||
- Fixed _adapter_disconnect in TornadoConnection class (Issue #237 - Fix by Tristan Penman)
|
||||
- Fixing hang when closing connection without any channel in BlockingConnection (Issue #244 - Fix by Ales Teska)
|
||||
- Remove the process_timeouts() call in SelectConnection (Issue #239)
|
||||
- Change the string validation to basestring for host connection parameters (Issue #231)
|
||||
- Add a poller to the BlockingConnection to address latency issues introduced in Pika 0.9.8 (Issue #242)
|
||||
- reply_code and reply_text is not set in ChannelException (Issue #250)
|
||||
- Add the missing constraint parameter for Channel._on_return callback processing (Issue #257 - Fix by patcpsc)
|
||||
- Channel callbacks not being removed from callback manager when channel is closed or deleted (Issue #261)
|
||||
|
||||
0.9.8 - 2012-11-18
|
||||
------------------
|
||||
|
||||
`0.9.8 <https://github.com/pika/pika/compare/0.9.7...0.9.8>`_
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Channel.queue_declare/BlockingChannel.queue_declare not setting up callbacks property for empty queue name (Issue #218)
|
||||
- Channel.queue_bind/BlockingChannel.queue_bind not allowing empty routing key
|
||||
- Connection._on_connection_closed calling wrong method in Channel (Issue #219)
|
||||
- Fix tx_commit and tx_rollback bugs in BlockingChannel (Issue #217)
|
||||
|
||||
0.9.7 - 2012-11-11
|
||||
------------------
|
||||
|
||||
`0.9.7 <https://github.com/pika/pika/compare/0.9.6...0.9.7>`_
|
||||
|
||||
**New features**
|
||||
|
||||
- generator based consumer in BlockingChannel (See :doc:`examples/blocking_consumer_generator` for example)
|
||||
|
||||
**Changes**
|
||||
|
||||
- BlockingChannel._send_method will only wait if explicitly told to
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Added the exchange "type" parameter back but issue a DeprecationWarning
|
||||
- Dont require a queue name in Channel.queue_declare()
|
||||
- Fixed KeyError when processing timeouts (Issue # 215 - Fix by Raphael De Giusti)
|
||||
- Don't try and close channels when the connection is closed (Issue #216 - Fix by Charles Law)
|
||||
- Dont raise UnexpectedFrame exceptions, log them instead
|
||||
- Handle multiple synchronous RPC calls made without waiting for the call result (Issues #192, #204, #211)
|
||||
- Typo in docs (Issue #207 Fix by Luca Wehrstedt)
|
||||
- Only sleep on connection failure when retry attempts are > 0 (Issue #200)
|
||||
- Bypass _rpc method and just send frames for Basic.Ack, Basic.Nack, Basic.Reject (Issue #205)
|
||||
|
||||
0.9.6 - 2012-10-29
|
||||
------------------
|
||||
|
||||
`0.9.6 <https://github.com/pika/pika/compare/0.9.5...0.9.6>`_
|
||||
|
||||
**New features**
|
||||
|
||||
- URLParameters
|
||||
- BlockingChannel.start_consuming() and BlockingChannel.stop_consuming()
|
||||
- Delivery Confirmations
|
||||
- Improved unittests
|
||||
|
||||
**Major bugfix areas**
|
||||
|
||||
- Connection handling
|
||||
- Blocking functionality in the BlockingConnection
|
||||
- SSL
|
||||
- UTF-8 Handling
|
||||
|
||||
**Removals**
|
||||
|
||||
- pika.reconnection_strategies
|
||||
- pika.channel.ChannelTransport
|
||||
- pika.log
|
||||
- pika.template
|
||||
- examples directory
|
||||
|
||||
0.9.5 - 2011-03-29
|
||||
------------------
|
||||
|
||||
`0.9.5 <https://github.com/pika/pika/compare/0.9.4...0.9.5>`_
|
||||
|
||||
**Changelog**
|
||||
|
||||
- Scope changes with adapter IOLoops and CallbackManager allowing for cleaner, multi-threaded operation
|
||||
- Add support for Confirm.Select with channel.Channel.confirm_delivery()
|
||||
- Add examples of delivery confirmation to examples (demo_send_confirmed.py)
|
||||
- Update uses of log.warn with warning.warn for TCP Back-pressure alerting
|
||||
- License boilerplate updated to simplify license text in source files
|
||||
- Increment the timeout in select_connection.SelectPoller reducing CPU utilization
|
||||
- Bug fix in Heartbeat frame delivery addressing issue #35
|
||||
- Remove abuse of pika.log.method_call through a majority of the code
|
||||
- Rename of key modules: table to data, frames to frame
|
||||
- Cleanup of frame module and related classes
|
||||
- Restructure of tests and test runner
|
||||
- Update functional tests to respect RABBITMQ_HOST, RABBITMQ_PORT environment variables
|
||||
- Bug fixes to reconnection_strategies module
|
||||
- Fix the scale of timeout for PollPoller to be specified in milliseconds
|
||||
- Remove mutable default arguments in RPC calls
|
||||
- Add data type validation to RPC calls
|
||||
- Move optional credentials erasing out of connection.Connection into credentials module
|
||||
- Add support to allow for additional external credential types
|
||||
- Add a NullHandler to prevent the 'No handlers could be found for logger "pika"' error message when not using pika.log in a client app at all.
|
||||
- Clean up all examples to make them easier to read and use
|
||||
- Move documentation into its own repository https://github.com/pika/documentation
|
||||
|
||||
- channel.py
|
||||
|
||||
- Move channel.MAX_CHANNELS constant from connection.CHANNEL_MAX
|
||||
- Add default value of None to ChannelTransport.rpc
|
||||
- Validate callback and acceptable replies parameters in ChannelTransport.RPC
|
||||
- Remove unused connection attribute from Channel
|
||||
|
||||
- connection.py
|
||||
|
||||
- Remove unused import of struct
|
||||
- Remove direct import of pika.credentials.PlainCredentials
|
||||
- Change to import pika.credentials
|
||||
- Move CHANNEL_MAX to channel.MAX_CHANNELS
|
||||
- Change ConnectionParameters initialization parameter heartbeat to boolean
|
||||
- Validate all inbound parameter types in ConnectionParameters
|
||||
- Remove the Connection._erase_credentials stub method in favor of letting the Credentials object deal with that itself.
|
||||
- Warn if the credentials object intends on erasing the credentials and a reconnection strategy other than NullReconnectionStrategy is specified.
|
||||
- Change the default types for callback and acceptable_replies in Connection._rpc
|
||||
- Validate the callback and acceptable_replies data types in Connection._rpc
|
||||
|
||||
- adapters.blocking_connection.BlockingConnection
|
||||
|
||||
- Addition of _adapter_disconnect to blocking_connection.BlockingConnection
|
||||
- Add timeout methods to BlockingConnection addressing issue #41
|
||||
- BlockingConnection didn't allow you register more than one consumer callback because basic_consume was overridden to block immediately. New behavior allows you to do so.
|
||||
- Removed overriding of base basic_consume and basic_cancel methods. Now uses underlying Channel versions of those methods.
|
||||
- Added start_consuming() method to BlockingChannel to start the consumption loop.
|
||||
- Updated stop_consuming() to iterate through all the registered consumers in self._consumers and issue a basic_cancel.
|
|
@ -0,0 +1,68 @@
|
|||
# Contributing
|
||||
|
||||
## Test Coverage
|
||||
|
||||
To contribute to Pika, please make sure that any new features or changes
|
||||
to existing functionality **include test coverage**.
|
||||
|
||||
*Pull requests that add or change code without coverage have a much lower chance
|
||||
of being accepted.*
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Pika test suite has a couple of requirements:
|
||||
|
||||
* Dependencies from `test-dependencies.txt` are installed
|
||||
* A RabbitMQ node with all defaults is running on `localhost:5672`
|
||||
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
To install the dependencies needed to run Pika tests, use
|
||||
|
||||
pip install -r test-requirements.txt
|
||||
|
||||
which on Python 3 might look like this
|
||||
|
||||
pip3 install -r test-requirements.txt
|
||||
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run all test suites, use
|
||||
|
||||
nosetests
|
||||
|
||||
Note that some tests are OS-specific (e.g. epoll on Linux
|
||||
or kqueue on MacOS and BSD). Those will be skipped
|
||||
automatically.
|
||||
|
||||
If you would like to run TLS/SSL tests, use the following procedure:
|
||||
|
||||
* Create a `rabbitmq.conf` file:
|
||||
|
||||
```
|
||||
sed -e "s#PIKA_DIR#$PWD#g" ./testdata/rabbitmq.conf.in > ./testdata/rabbitmq.conf
|
||||
```
|
||||
|
||||
* Start RabbitMQ and use the configuration file you just created. An example command
|
||||
that works with the `generic-unix` package is as follows:
|
||||
|
||||
```
|
||||
$ RABBITMQ_CONFIG_FILE=/path/to/pika/testdata/rabbitmq.conf ./sbin/rabbitmq-server
|
||||
```
|
||||
|
||||
* Run the tests indicating that TLS/SSL connections should be used:
|
||||
|
||||
```
|
||||
PIKA_TEST_TLS=true nosetests
|
||||
```
|
||||
|
||||
|
||||
## Code Formatting
|
||||
|
||||
Please format your code using [yapf](http://pypi.python.org/pypi/yapf)
|
||||
with ``google`` style prior to issuing your pull request. *Note: only format those
|
||||
lines that you have changed in your pull request. If you format an entire file and
|
||||
change code outside of the scope of your PR, it will likely be rejected.*
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2009-2017, Tony Garnock-Jones, Gavin M. Roy, Pivotal and others.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of the Pika project nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,2 @@
|
|||
include LICENSE
|
||||
include README.rst
|
|
@ -0,0 +1,157 @@
|
|||
Pika
|
||||
====
|
||||
Pika is a RabbitMQ (AMQP-0-9-1) client library for Python.
|
||||
|
||||
|Version| |Python versions| |Status| |Coverage| |License| |Docs|
|
||||
|
||||
Introduction
|
||||
-------------
|
||||
Pika is a pure-Python implementation of the AMQP 0-9-1 protocol including RabbitMQ's
|
||||
extensions.
|
||||
|
||||
- Python 2.7 and 3.4+ are supported.
|
||||
|
||||
- Since threads aren't appropriate to every situation, it doesn't
|
||||
require threads. It takes care not to forbid them, either. The same
|
||||
goes for greenlets, callbacks, continuations and generators. It is
|
||||
not necessarily thread-safe however, and your mileage will vary.
|
||||
|
||||
- People may be using direct sockets, plain old `select()`,
|
||||
or any of the wide variety of ways of getting network events to and from a
|
||||
python application. Pika tries to stay compatible with all of these, and to
|
||||
make adapting it to a new environment as simple as possible.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
Pika's documentation can be found at `https://pika.readthedocs.io <https://pika.readthedocs.io>`_
|
||||
|
||||
Example
|
||||
-------
|
||||
Here is the most simple example of use, sending a message with the BlockingConnection adapter:
|
||||
|
||||
.. code :: python
|
||||
|
||||
import pika
|
||||
connection = pika.BlockingConnection()
|
||||
channel = connection.channel()
|
||||
channel.basic_publish(exchange='example',
|
||||
routing_key='test',
|
||||
body='Test Message')
|
||||
connection.close()
|
||||
|
||||
And an example of writing a blocking consumer:
|
||||
|
||||
.. code :: python
|
||||
|
||||
import pika
|
||||
connection = pika.BlockingConnection()
|
||||
channel = connection.channel()
|
||||
|
||||
for method_frame, properties, body in channel.consume('test'):
|
||||
|
||||
# Display the message parts and ack the message
|
||||
print(method_frame, properties, body)
|
||||
channel.basic_ack(method_frame.delivery_tag)
|
||||
|
||||
# Escape out of the loop after 10 messages
|
||||
if method_frame.delivery_tag == 10:
|
||||
break
|
||||
|
||||
# Cancel the consumer and return any pending messages
|
||||
requeued_messages = channel.cancel()
|
||||
print('Requeued %i messages' % requeued_messages)
|
||||
connection.close()
|
||||
|
||||
Pika provides the following adapters
|
||||
------------------------------------
|
||||
|
||||
- AsyncioConnection - adapter for the Python3 AsyncIO event loop
|
||||
- BlockingConnection - enables blocking, synchronous operation on top of library for simple uses
|
||||
- SelectConnection - fast asynchronous adapter
|
||||
- TornadoConnection - adapter for use with the Tornado IO Loop http://tornadoweb.org
|
||||
- TwistedConnection - adapter for use with the Twisted asynchronous package http://twistedmatrix.com/
|
||||
|
||||
Requesting message ACKs from another thread
|
||||
-------------------------------------------
|
||||
The single-threaded usage constraint of an individual Pika connection adapter
|
||||
instance may result in a dropped AMQP/stream connection due to AMQP heartbeat
|
||||
timeout in consumers that take a long time to process an incoming message. A
|
||||
common solution is to delegate processing of the incoming messages to another
|
||||
thread, while the connection adapter's thread continues to service its ioloop's
|
||||
message pump, permitting AMQP heartbeats and other I/O to be serviced in a
|
||||
timely fashion.
|
||||
|
||||
Messages processed in another thread may not be ACK'ed directly from that thread,
|
||||
since all accesses to the connection adapter instance must be from a single
|
||||
thread - the thread that is running the adapter's ioloop. However, this may be
|
||||
accomplished by requesting a callback to be executed in the adapter's ioloop
|
||||
thread. For example, the callback function's implementation might look like this:
|
||||
|
||||
.. code :: python
|
||||
|
||||
def ack_message(channel, delivery_tag):
|
||||
"""Note that `channel` must be the same pika channel instance via which
|
||||
the message being ACKed was retrieved (AMQP protocol constraint).
|
||||
"""
|
||||
if channel.is_open:
|
||||
channel.basic_ack(delivery_tag)
|
||||
else:
|
||||
# Channel is already closed, so we can't ACK this message;
|
||||
# log and/or do something that makes sense for your app in this case.
|
||||
pass
|
||||
|
||||
The code running in the other thread may request the `ack_message()` function
|
||||
to be executed in the connection adapter's ioloop thread using an
|
||||
adapter-specific mechanism:
|
||||
|
||||
- :py:class:`pika.BlockingConnection` abstracts its ioloop from the application
|
||||
and thus exposes :py:meth:`pika.BlockingConnection.add_callback_threadsafe()`.
|
||||
Refer to this method's docstring for additional information. For example:
|
||||
|
||||
.. code :: python
|
||||
|
||||
connection.add_callback_threadsafe(functools.partial(ack_message, channel, delivery_tag))
|
||||
|
||||
- When using a non-blocking connection adapter, such as
|
||||
:py:class:`pika.adapters.asyncio_connection.AsyncioConnection` or
|
||||
:py:class:`pika.SelectConnection`, you use the underlying asynchronous
|
||||
framework's native API for requesting an ioloop-bound callback from
|
||||
another thread. For example, `SelectConnection`'s `IOLoop` provides
|
||||
`add_callback_threadsafe()`, `Tornado`'s `IOLoop` has
|
||||
`add_callback()`, while `asyncio`'s event loop exposes
|
||||
`call_soon_threadsafe()`.
|
||||
|
||||
This threadsafe callback request mechanism may also be used to delegate
|
||||
publishing of messages, etc., from a background thread to the connection adapter's
|
||||
thread.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
To contribute to pika, please make sure that any new features or changes
|
||||
to existing functionality **include test coverage**.
|
||||
|
||||
*Pull requests that add or change code without coverage will most likely be rejected.*
|
||||
|
||||
Additionally, please format your code using `yapf <http://pypi.python.org/pypi/yapf>`_
|
||||
with ``google`` style prior to issuing your pull request. *Note: only format those
|
||||
lines that you have changed in your pull request. If you format an entire file and
|
||||
change code outside of the scope of your PR, it will likely be rejected.*
|
||||
|
||||
.. |Version| image:: https://img.shields.io/pypi/v/pika.svg?
|
||||
:target: http://badge.fury.io/py/pika
|
||||
|
||||
.. |Python versions| image:: https://img.shields.io/pypi/pyversions/pika.svg
|
||||
:target: https://pypi.python.org/pypi/pika
|
||||
|
||||
.. |Status| image:: https://img.shields.io/travis/pika/pika.svg?
|
||||
:target: https://travis-ci.org/pika/pika
|
||||
|
||||
.. |Coverage| image:: https://img.shields.io/codecov/c/github/pika/pika.svg?
|
||||
:target: https://codecov.io/github/pika/pika?branch=master
|
||||
|
||||
.. |License| image:: https://img.shields.io/pypi/l/pika.svg?
|
||||
:target: https://pika.readthedocs.io
|
||||
|
||||
.. |Docs| image:: https://readthedocs.org/projects/pika/badge/?version=stable
|
||||
:target: https://pika.readthedocs.io
|
||||
:alt: Documentation Status
|
|
@ -0,0 +1,107 @@
|
|||
# Windows build and test of Pika
|
||||
|
||||
environment:
|
||||
erlang_download_url: "http://erlang.org/download/otp_win64_19.3.exe"
|
||||
erlang_exe_path: "C:\\Users\\appveyor\\erlang_19.3.exe"
|
||||
erlang_home_dir: "C:\\Users\\appveyor\\erlang"
|
||||
erlang_erts_version: "erts-8.3"
|
||||
|
||||
rabbitmq_version: 3.7.4
|
||||
rabbitmq_installer_download_url: "https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.7.4/rabbitmq-server-3.7.4.exe"
|
||||
rabbitmq_installer_path: "C:\\Users\\appveyor\\rabbitmq-server-3.7.4.exe"
|
||||
|
||||
matrix:
|
||||
- PYTHON_ARCH: "32"
|
||||
PYTHONHOME: "C:\\Python27"
|
||||
PIKA_TEST_TLS: false
|
||||
- PYTHON_ARCH: "32"
|
||||
PYTHONHOME: "C:\\Python27"
|
||||
PIKA_TEST_TLS: true
|
||||
|
||||
|
||||
cache:
|
||||
# RabbitMQ is a pretty big package, so caching it in hopes of expediting the
|
||||
# runtime
|
||||
- "%erlang_exe_path%"
|
||||
- "%rabbitmq_installer_path%"
|
||||
|
||||
|
||||
install:
|
||||
- SET PYTHONPATH=%PYTHONHOME%
|
||||
- SET PATH=%PYTHONHOME%\Scripts;%PYTHONHOME%;%PATH%
|
||||
|
||||
# For diagnostics
|
||||
- ECHO %PYTHONPATH%
|
||||
- ECHO %PATH%
|
||||
- python --version
|
||||
|
||||
- ECHO Upgrading pip...
|
||||
- python -m pip install --upgrade pip setuptools
|
||||
- pip --version
|
||||
|
||||
- ECHO Installing wheel...
|
||||
- pip install wheel
|
||||
|
||||
|
||||
build_script:
|
||||
- ECHO Building distributions...
|
||||
- python setup.py sdist bdist bdist_wheel
|
||||
- DIR /s *.whl
|
||||
|
||||
|
||||
artifacts:
|
||||
- path: 'dist\*.whl'
|
||||
name: pika wheel
|
||||
|
||||
|
||||
before_test:
|
||||
# Install test requirements
|
||||
- ECHO Installing pika...
|
||||
- python setup.py install
|
||||
|
||||
- ECHO Installing pika test requirements...
|
||||
- pip install -r test-requirements.txt
|
||||
|
||||
# List conents of C:\ to help debug caching of rabbitmq artifacts
|
||||
# - DIR C:\
|
||||
|
||||
- ps: $webclient=New-Object System.Net.WebClient
|
||||
|
||||
- ECHO Downloading Erlang...
|
||||
- ps: if (-Not (Test-Path "$env:erlang_exe_path")) { $webclient.DownloadFile("$env:erlang_download_url", "$env:erlang_exe_path") } else { Write-Host "Found" $env:erlang_exe_path "in cache." }
|
||||
|
||||
- ECHO Installing Erlang...
|
||||
- start /B /WAIT %erlang_exe_path% /S /D=%erlang_home_dir%
|
||||
- set ERLANG_HOME=%erlang_home_dir%
|
||||
|
||||
- ECHO Downloading RabbitMQ...
|
||||
- ps: if (-Not (Test-Path "$env:rabbitmq_installer_path")) { $webclient.DownloadFile("$env:rabbitmq_installer_download_url", "$env:rabbitmq_installer_path") } else { Write-Host "Found" $env:rabbitmq_installer_path "in cache." }
|
||||
|
||||
- ECHO Creating directory %AppData%\RabbitMQ...
|
||||
- ps: New-Item -ItemType Directory -ErrorAction Continue -Path "$env:AppData/RabbitMQ"
|
||||
|
||||
- ECHO Creating RabbitMQ configuration file in %AppData%\RabbitMQ...
|
||||
- ps: Get-Content C:/Projects/pika/testdata/rabbitmq.conf.in | %{ $_ -replace 'PIKA_DIR', 'C:/projects/pika' } | Set-Content -Path "$env:AppData/RabbitMQ/rabbitmq.conf"
|
||||
- ps: Get-Content "$env:AppData/RabbitMQ/rabbitmq.conf"
|
||||
|
||||
- ECHO Creating Erlang cookie files...
|
||||
- ps: '[System.IO.File]::WriteAllText("C:\Users\appveyor\.erlang.cookie", "PIKAISTHEBEST", [System.Text.Encoding]::ASCII)'
|
||||
- ps: '[System.IO.File]::WriteAllText("C:\Windows\System32\config\systemprofile\.erlang.cookie", "PIKAISTHEBEST", [System.Text.Encoding]::ASCII)'
|
||||
|
||||
- ECHO Installing and starting RabbitMQ with default config...
|
||||
- start /B /WAIT %rabbitmq_installer_path% /S
|
||||
- ps: (Get-Service -Name RabbitMQ).Status
|
||||
|
||||
- ECHO Waiting for epmd to report that RabbitMQ has started...
|
||||
- ps: 'C:\projects\pika\testdata\wait-epmd.ps1'
|
||||
- ps: 'C:\projects\pika\testdata\wait-rabbitmq.ps1'
|
||||
|
||||
- ECHO Getting RabbitMQ status...
|
||||
- cmd /c "C:\Program Files\RabbitMQ Server\rabbitmq_server-%rabbitmq_version%\sbin\rabbitmqctl.bat" status
|
||||
|
||||
|
||||
test_script:
|
||||
- nosetests
|
||||
|
||||
# Since Pika is source-only there's no need to deploy from Windows
|
||||
deploy: false
|
|
@ -0,0 +1,153 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pika.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pika.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/pika"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pika"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
|
@ -0,0 +1,34 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
sys.path.insert(0, '../')
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode',
|
||||
'sphinx.ext.intersphinx']
|
||||
|
||||
intersphinx_mapping = {'python': ('https://docs.python.org/3/',
|
||||
'https://docs.python.org/3/objects.inv'),
|
||||
'tornado': ('http://www.tornadoweb.org/en/stable/',
|
||||
'http://www.tornadoweb.org/en/stable/objects.inv')}
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
project = 'pika'
|
||||
copyright = '2009-2017, Tony Garnock-Jones, Gavin M. Roy, Pivotal Software, Inc and contributors.'
|
||||
|
||||
import pika
|
||||
release = pika.__version__
|
||||
version = '.'.join(release.split('.')[0:1])
|
||||
|
||||
exclude_patterns = ['_build']
|
||||
add_function_parentheses = True
|
||||
add_module_names = True
|
||||
show_authors = True
|
||||
pygments_style = 'sphinx'
|
||||
modindex_common_prefix = ['pika']
|
||||
html_theme = 'default'
|
||||
html_static_path = ['_static']
|
||||
htmlhelp_basename = 'pikadoc'
|
|
@ -0,0 +1,104 @@
|
|||
Contributors
|
||||
============
|
||||
The following people have directly contributes code by way of new features and/or bug fixes to Pika:
|
||||
|
||||
- Gavin M. Roy
|
||||
- Tony Garnock-Jones
|
||||
- Vitaly Kruglikov
|
||||
- Michael Laing
|
||||
- Marek Majkowski
|
||||
- Jan Urbański
|
||||
- Brian K. Jones
|
||||
- Ask Solem
|
||||
- ml
|
||||
- Will
|
||||
- atatsu
|
||||
- Fredrik Svensson
|
||||
- Pedro Abranches
|
||||
- Kyösti Herrala
|
||||
- Erik Andersson
|
||||
- Charles Law
|
||||
- Alex Chandel
|
||||
- Tristan Penman
|
||||
- Raphaël De Giusti
|
||||
- Jozef Van Eenbergen
|
||||
- Josh Braegger
|
||||
- Jason J. W. Williams
|
||||
- James Mutton
|
||||
- Cenk Alti
|
||||
- Asko Soukka
|
||||
- Antti Haapala
|
||||
- Anton Ryzhov
|
||||
- cellscape
|
||||
- cacovsky
|
||||
- bra-fsn
|
||||
- ateska
|
||||
- Roey Berman
|
||||
- Robert Weidlich
|
||||
- Riccardo Cirimelli
|
||||
- Perttu Ranta-aho
|
||||
- Pau Gargallo
|
||||
- Kane
|
||||
- Kamil Kisiel
|
||||
- Jonty Wareing
|
||||
- Jonathan Kirsch
|
||||
- Jacek 'Forger' Całusiński
|
||||
- Garth Williamson
|
||||
- Erik Olof Gunnar Andersson
|
||||
- David Strauss
|
||||
- Anton V. Yanchenko
|
||||
- Alexey Myasnikov
|
||||
- Alessandro Tagliapietra
|
||||
- Adam Flynn
|
||||
- skftn
|
||||
- saarni
|
||||
- pavlobaron
|
||||
- nonleaf
|
||||
- markcf
|
||||
- george y
|
||||
- eivanov
|
||||
- bstemshorn
|
||||
- a-tal
|
||||
- Yang Yang
|
||||
- Stuart Longland
|
||||
- Sigurd Høgsbro
|
||||
- Sean Dwyer
|
||||
- Samuel Stauffer
|
||||
- Roberto Decurnex
|
||||
- Rikard Hultén
|
||||
- Richard Boulton
|
||||
- Ralf Nyren
|
||||
- Qi Fan
|
||||
- Peter Magnusson
|
||||
- Pankrat
|
||||
- Olivier Le Thanh Duong
|
||||
- Njal Karevoll
|
||||
- Milan Skuhra
|
||||
- Mik Kocikowski
|
||||
- Michael Kenney
|
||||
- Mark Unsworth
|
||||
- Luca Wehrstedt
|
||||
- Laurent Eschenauer
|
||||
- Lars van de Kerkhof
|
||||
- Kyösti Herrala
|
||||
- Juhyeong Park
|
||||
- JuhaS
|
||||
- Josh Hansen
|
||||
- Jorge Puente Sarrín
|
||||
- Jeff Tang
|
||||
- Jeff Fein-Worton
|
||||
- Jeff
|
||||
- Hunter Morris
|
||||
- Guruprasad
|
||||
- Garrett Cooper
|
||||
- Frank Slaughter
|
||||
- Dustin Koupal
|
||||
- Bjorn Sandberg
|
||||
- Axel Eirola
|
||||
- Andrew Smith
|
||||
- Andrew Grigorev
|
||||
- Andrew
|
||||
- Allard Hoeve
|
||||
- A.Shaposhnikov
|
||||
|
||||
*Contributors listed by commit count.*
|
|
@ -0,0 +1,23 @@
|
|||
Usage Examples
|
||||
==============
|
||||
|
||||
Pika has various methods of use, between the synchronous BlockingConnection adapter and the various asynchronous connection adapter. The following examples illustrate the various ways that you can use Pika in your projects.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:maxdepth: 1
|
||||
|
||||
examples/using_urlparameters
|
||||
examples/connecting_async
|
||||
examples/blocking_basic_get
|
||||
examples/blocking_consume
|
||||
examples/blocking_consumer_generator
|
||||
examples/comparing_publishing_sync_async
|
||||
examples/blocking_delivery_confirmations
|
||||
examples/blocking_publish_mandatory
|
||||
examples/asynchronous_consumer_example
|
||||
examples/asynchronous_publisher_example
|
||||
examples/twisted_example
|
||||
examples/tornado_consumer
|
||||
examples/tls_mutual_authentication
|
||||
examples/tls_server_authentication
|
|
@ -0,0 +1,357 @@
|
|||
Asynchronous consumer example
|
||||
=============================
|
||||
The following example implements a consumer that will respond to RPC commands sent from RabbitMQ. For example, it will reconnect if RabbitMQ closes the connection and will shutdown if RabbitMQ cancels the consumer or closes the channel. While it may look intimidating, each method is very short and represents a individual actions that a consumer can do.
|
||||
|
||||
consumer.py::
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import pika
|
||||
|
||||
LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) '
|
||||
'-35s %(lineno) -5d: %(message)s')
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExampleConsumer(object):
|
||||
"""This is an example consumer that will handle unexpected interactions
|
||||
with RabbitMQ such as channel and connection closures.
|
||||
|
||||
If RabbitMQ closes the connection, it will reopen it. You should
|
||||
look at the output, as there are limited reasons why the connection may
|
||||
be closed, which usually are tied to permission related issues or
|
||||
socket timeouts.
|
||||
|
||||
If the channel is closed, it will indicate a problem with one of the
|
||||
commands that were issued and that should surface in the output as well.
|
||||
|
||||
"""
|
||||
EXCHANGE = 'message'
|
||||
EXCHANGE_TYPE = 'topic'
|
||||
QUEUE = 'text'
|
||||
ROUTING_KEY = 'example.text'
|
||||
|
||||
def __init__(self, amqp_url):
|
||||
"""Create a new instance of the consumer class, passing in the AMQP
|
||||
URL used to connect to RabbitMQ.
|
||||
|
||||
:param str amqp_url: The AMQP url to connect with
|
||||
|
||||
"""
|
||||
self._connection = None
|
||||
self._channel = None
|
||||
self._closing = False
|
||||
self._consumer_tag = None
|
||||
self._url = amqp_url
|
||||
|
||||
def connect(self):
|
||||
"""This method connects to RabbitMQ, returning the connection handle.
|
||||
When the connection is established, the on_connection_open method
|
||||
will be invoked by pika.
|
||||
|
||||
:rtype: pika.SelectConnection
|
||||
|
||||
"""
|
||||
LOGGER.info('Connecting to %s', self._url)
|
||||
return pika.SelectConnection(pika.URLParameters(self._url),
|
||||
self.on_connection_open,
|
||||
stop_ioloop_on_close=False)
|
||||
|
||||
def on_connection_open(self, unused_connection):
|
||||
"""This method is called by pika once the connection to RabbitMQ has
|
||||
been established. It passes the handle to the connection object in
|
||||
case we need it, but in this case, we'll just mark it unused.
|
||||
|
||||
:type unused_connection: pika.SelectConnection
|
||||
|
||||
"""
|
||||
LOGGER.info('Connection opened')
|
||||
self.add_on_connection_close_callback()
|
||||
self.open_channel()
|
||||
|
||||
def add_on_connection_close_callback(self):
|
||||
"""This method adds an on close callback that will be invoked by pika
|
||||
when RabbitMQ closes the connection to the publisher unexpectedly.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding connection close callback')
|
||||
self._connection.add_on_close_callback(self.on_connection_closed)
|
||||
|
||||
def on_connection_closed(self, connection, reply_code, reply_text):
|
||||
"""This method is invoked by pika when the connection to RabbitMQ is
|
||||
closed unexpectedly. Since it is unexpected, we will reconnect to
|
||||
RabbitMQ if it disconnects.
|
||||
|
||||
:param pika.connection.Connection connection: The closed connection obj
|
||||
:param int reply_code: The server provided reply_code if given
|
||||
:param str reply_text: The server provided reply_text if given
|
||||
|
||||
"""
|
||||
self._channel = None
|
||||
if self._closing:
|
||||
self._connection.ioloop.stop()
|
||||
else:
|
||||
LOGGER.warning('Connection closed, reopening in 5 seconds: (%s) %s',
|
||||
reply_code, reply_text)
|
||||
self._connection.add_timeout(5, self.reconnect)
|
||||
|
||||
def reconnect(self):
|
||||
"""Will be invoked by the IOLoop timer if the connection is
|
||||
closed. See the on_connection_closed method.
|
||||
|
||||
"""
|
||||
# This is the old connection IOLoop instance, stop its ioloop
|
||||
self._connection.ioloop.stop()
|
||||
|
||||
if not self._closing:
|
||||
|
||||
# Create a new connection
|
||||
self._connection = self.connect()
|
||||
|
||||
# There is now a new connection, needs a new ioloop to run
|
||||
self._connection.ioloop.start()
|
||||
|
||||
def open_channel(self):
|
||||
"""Open a new channel with RabbitMQ by issuing the Channel.Open RPC
|
||||
command. When RabbitMQ responds that the channel is open, the
|
||||
on_channel_open callback will be invoked by pika.
|
||||
|
||||
"""
|
||||
LOGGER.info('Creating a new channel')
|
||||
self._connection.channel(on_open_callback=self.on_channel_open)
|
||||
|
||||
def on_channel_open(self, channel):
|
||||
"""This method is invoked by pika when the channel has been opened.
|
||||
The channel object is passed in so we can make use of it.
|
||||
|
||||
Since the channel is now open, we'll declare the exchange to use.
|
||||
|
||||
:param pika.channel.Channel channel: The channel object
|
||||
|
||||
"""
|
||||
LOGGER.info('Channel opened')
|
||||
self._channel = channel
|
||||
self.add_on_channel_close_callback()
|
||||
self.setup_exchange(self.EXCHANGE)
|
||||
|
||||
def add_on_channel_close_callback(self):
|
||||
"""This method tells pika to call the on_channel_closed method if
|
||||
RabbitMQ unexpectedly closes the channel.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding channel close callback')
|
||||
self._channel.add_on_close_callback(self.on_channel_closed)
|
||||
|
||||
def on_channel_closed(self, channel, reply_code, reply_text):
|
||||
"""Invoked by pika when RabbitMQ unexpectedly closes the channel.
|
||||
Channels are usually closed if you attempt to do something that
|
||||
violates the protocol, such as re-declare an exchange or queue with
|
||||
different parameters. In this case, we'll close the connection
|
||||
to shutdown the object.
|
||||
|
||||
:param pika.channel.Channel: The closed channel
|
||||
:param int reply_code: The numeric reason the channel was closed
|
||||
:param str reply_text: The text reason the channel was closed
|
||||
|
||||
"""
|
||||
LOGGER.warning('Channel %i was closed: (%s) %s',
|
||||
channel, reply_code, reply_text)
|
||||
self._connection.close()
|
||||
|
||||
def setup_exchange(self, exchange_name):
|
||||
"""Setup the exchange on RabbitMQ by invoking the Exchange.Declare RPC
|
||||
command. When it is complete, the on_exchange_declareok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param str|unicode exchange_name: The name of the exchange to declare
|
||||
|
||||
"""
|
||||
LOGGER.info('Declaring exchange %s', exchange_name)
|
||||
self._channel.exchange_declare(self.on_exchange_declareok,
|
||||
exchange_name,
|
||||
self.EXCHANGE_TYPE)
|
||||
|
||||
def on_exchange_declareok(self, unused_frame):
|
||||
"""Invoked by pika when RabbitMQ has finished the Exchange.Declare RPC
|
||||
command.
|
||||
|
||||
:param pika.Frame.Method unused_frame: Exchange.DeclareOk response frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Exchange declared')
|
||||
self.setup_queue(self.QUEUE)
|
||||
|
||||
def setup_queue(self, queue_name):
|
||||
"""Setup the queue on RabbitMQ by invoking the Queue.Declare RPC
|
||||
command. When it is complete, the on_queue_declareok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param str|unicode queue_name: The name of the queue to declare.
|
||||
|
||||
"""
|
||||
LOGGER.info('Declaring queue %s', queue_name)
|
||||
self._channel.queue_declare(self.on_queue_declareok, queue_name)
|
||||
|
||||
def on_queue_declareok(self, method_frame):
|
||||
"""Method invoked by pika when the Queue.Declare RPC call made in
|
||||
setup_queue has completed. In this method we will bind the queue
|
||||
and exchange together with the routing key by issuing the Queue.Bind
|
||||
RPC command. When this command is complete, the on_bindok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param pika.frame.Method method_frame: The Queue.DeclareOk frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Binding %s to %s with %s',
|
||||
self.EXCHANGE, self.QUEUE, self.ROUTING_KEY)
|
||||
self._channel.queue_bind(self.on_bindok, self.QUEUE,
|
||||
self.EXCHANGE, self.ROUTING_KEY)
|
||||
|
||||
def on_bindok(self, unused_frame):
|
||||
"""Invoked by pika when the Queue.Bind method has completed. At this
|
||||
point we will start consuming messages by calling start_consuming
|
||||
which will invoke the needed RPC commands to start the process.
|
||||
|
||||
:param pika.frame.Method unused_frame: The Queue.BindOk response frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Queue bound')
|
||||
self.start_consuming()
|
||||
|
||||
def start_consuming(self):
|
||||
"""This method sets up the consumer by first calling
|
||||
add_on_cancel_callback so that the object is notified if RabbitMQ
|
||||
cancels the consumer. It then issues the Basic.Consume RPC command
|
||||
which returns the consumer tag that is used to uniquely identify the
|
||||
consumer with RabbitMQ. We keep the value to use it when we want to
|
||||
cancel consuming. The on_message method is passed in as a callback pika
|
||||
will invoke when a message is fully received.
|
||||
|
||||
"""
|
||||
LOGGER.info('Issuing consumer related RPC commands')
|
||||
self.add_on_cancel_callback()
|
||||
self._consumer_tag = self._channel.basic_consume(self.on_message,
|
||||
self.QUEUE)
|
||||
|
||||
def add_on_cancel_callback(self):
|
||||
"""Add a callback that will be invoked if RabbitMQ cancels the consumer
|
||||
for some reason. If RabbitMQ does cancel the consumer,
|
||||
on_consumer_cancelled will be invoked by pika.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding consumer cancellation callback')
|
||||
self._channel.add_on_cancel_callback(self.on_consumer_cancelled)
|
||||
|
||||
def on_consumer_cancelled(self, method_frame):
|
||||
"""Invoked by pika when RabbitMQ sends a Basic.Cancel for a consumer
|
||||
receiving messages.
|
||||
|
||||
:param pika.frame.Method method_frame: The Basic.Cancel frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Consumer was cancelled remotely, shutting down: %r',
|
||||
method_frame)
|
||||
if self._channel:
|
||||
self._channel.close()
|
||||
|
||||
def on_message(self, unused_channel, basic_deliver, properties, body):
|
||||
"""Invoked by pika when a message is delivered from RabbitMQ. The
|
||||
channel is passed for your convenience. The basic_deliver object that
|
||||
is passed in carries the exchange, routing key, delivery tag and
|
||||
a redelivered flag for the message. The properties passed in is an
|
||||
instance of BasicProperties with the message properties and the body
|
||||
is the message that was sent.
|
||||
|
||||
:param pika.channel.Channel unused_channel: The channel object
|
||||
:param pika.Spec.Basic.Deliver: basic_deliver method
|
||||
:param pika.Spec.BasicProperties: properties
|
||||
:param str|unicode body: The message body
|
||||
|
||||
"""
|
||||
LOGGER.info('Received message # %s from %s: %s',
|
||||
basic_deliver.delivery_tag, properties.app_id, body)
|
||||
self.acknowledge_message(basic_deliver.delivery_tag)
|
||||
|
||||
def acknowledge_message(self, delivery_tag):
|
||||
"""Acknowledge the message delivery from RabbitMQ by sending a
|
||||
Basic.Ack RPC method for the delivery tag.
|
||||
|
||||
:param int delivery_tag: The delivery tag from the Basic.Deliver frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Acknowledging message %s', delivery_tag)
|
||||
self._channel.basic_ack(delivery_tag)
|
||||
|
||||
def stop_consuming(self):
|
||||
"""Tell RabbitMQ that you would like to stop consuming by sending the
|
||||
Basic.Cancel RPC command.
|
||||
|
||||
"""
|
||||
if self._channel:
|
||||
LOGGER.info('Sending a Basic.Cancel RPC command to RabbitMQ')
|
||||
self._channel.basic_cancel(self.on_cancelok, self._consumer_tag)
|
||||
|
||||
def on_cancelok(self, unused_frame):
|
||||
"""This method is invoked by pika when RabbitMQ acknowledges the
|
||||
cancellation of a consumer. At this point we will close the channel.
|
||||
This will invoke the on_channel_closed method once the channel has been
|
||||
closed, which will in-turn close the connection.
|
||||
|
||||
:param pika.frame.Method unused_frame: The Basic.CancelOk frame
|
||||
|
||||
"""
|
||||
LOGGER.info('RabbitMQ acknowledged the cancellation of the consumer')
|
||||
self.close_channel()
|
||||
|
||||
def close_channel(self):
|
||||
"""Call to close the channel with RabbitMQ cleanly by issuing the
|
||||
Channel.Close RPC command.
|
||||
|
||||
"""
|
||||
LOGGER.info('Closing the channel')
|
||||
self._channel.close()
|
||||
|
||||
def run(self):
|
||||
"""Run the example consumer by connecting to RabbitMQ and then
|
||||
starting the IOLoop to block and allow the SelectConnection to operate.
|
||||
|
||||
"""
|
||||
self._connection = self.connect()
|
||||
self._connection.ioloop.start()
|
||||
|
||||
def stop(self):
|
||||
"""Cleanly shutdown the connection to RabbitMQ by stopping the consumer
|
||||
with RabbitMQ. When RabbitMQ confirms the cancellation, on_cancelok
|
||||
will be invoked by pika, which will then closing the channel and
|
||||
connection. The IOLoop is started again because this method is invoked
|
||||
when CTRL-C is pressed raising a KeyboardInterrupt exception. This
|
||||
exception stops the IOLoop which needs to be running for pika to
|
||||
communicate with RabbitMQ. All of the commands issued prior to starting
|
||||
the IOLoop will be buffered but not processed.
|
||||
|
||||
"""
|
||||
LOGGER.info('Stopping')
|
||||
self._closing = True
|
||||
self.stop_consuming()
|
||||
self._connection.ioloop.start()
|
||||
LOGGER.info('Stopped')
|
||||
|
||||
def close_connection(self):
|
||||
"""This method closes the connection to RabbitMQ."""
|
||||
LOGGER.info('Closing connection')
|
||||
self._connection.close()
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
|
||||
example = ExampleConsumer('amqp://guest:guest@localhost:5672/%2F')
|
||||
try:
|
||||
example.run()
|
||||
except KeyboardInterrupt:
|
||||
example.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,359 @@
|
|||
Asynchronous publisher example
|
||||
==============================
|
||||
The following example implements a publisher that will respond to RPC commands sent from RabbitMQ and uses delivery confirmations. It will reconnect if RabbitMQ closes the connection and will shutdown if RabbitMQ closes the channel. While it may look intimidating, each method is very short and represents a individual actions that a publisher can do.
|
||||
|
||||
publisher.py::
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
import pika
|
||||
import json
|
||||
|
||||
LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) '
|
||||
'-35s %(lineno) -5d: %(message)s')
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExamplePublisher(object):
|
||||
"""This is an example publisher that will handle unexpected interactions
|
||||
with RabbitMQ such as channel and connection closures.
|
||||
|
||||
If RabbitMQ closes the connection, it will reopen it. You should
|
||||
look at the output, as there are limited reasons why the connection may
|
||||
be closed, which usually are tied to permission related issues or
|
||||
socket timeouts.
|
||||
|
||||
It uses delivery confirmations and illustrates one way to keep track of
|
||||
messages that have been sent and if they've been confirmed by RabbitMQ.
|
||||
|
||||
"""
|
||||
EXCHANGE = 'message'
|
||||
EXCHANGE_TYPE = 'topic'
|
||||
PUBLISH_INTERVAL = 1
|
||||
QUEUE = 'text'
|
||||
ROUTING_KEY = 'example.text'
|
||||
|
||||
def __init__(self, amqp_url):
|
||||
"""Setup the example publisher object, passing in the URL we will use
|
||||
to connect to RabbitMQ.
|
||||
|
||||
:param str amqp_url: The URL for connecting to RabbitMQ
|
||||
|
||||
"""
|
||||
self._connection = None
|
||||
self._channel = None
|
||||
|
||||
self._deliveries = None
|
||||
self._acked = None
|
||||
self._nacked = None
|
||||
self._message_number = None
|
||||
|
||||
self._stopping = False
|
||||
self._url = amqp_url
|
||||
|
||||
def connect(self):
|
||||
"""This method connects to RabbitMQ, returning the connection handle.
|
||||
When the connection is established, the on_connection_open method
|
||||
will be invoked by pika. If you want the reconnection to work, make
|
||||
sure you set stop_ioloop_on_close to False, which is not the default
|
||||
behavior of this adapter.
|
||||
|
||||
:rtype: pika.SelectConnection
|
||||
|
||||
"""
|
||||
LOGGER.info('Connecting to %s', self._url)
|
||||
return pika.SelectConnection(pika.URLParameters(self._url),
|
||||
on_open_callback=self.on_connection_open,
|
||||
on_close_callback=self.on_connection_closed,
|
||||
stop_ioloop_on_close=False)
|
||||
|
||||
def on_connection_open(self, unused_connection):
|
||||
"""This method is called by pika once the connection to RabbitMQ has
|
||||
been established. It passes the handle to the connection object in
|
||||
case we need it, but in this case, we'll just mark it unused.
|
||||
|
||||
:type unused_connection: pika.SelectConnection
|
||||
|
||||
"""
|
||||
LOGGER.info('Connection opened')
|
||||
self.open_channel()
|
||||
|
||||
def on_connection_closed(self, connection, reply_code, reply_text):
|
||||
"""This method is invoked by pika when the connection to RabbitMQ is
|
||||
closed unexpectedly. Since it is unexpected, we will reconnect to
|
||||
RabbitMQ if it disconnects.
|
||||
|
||||
:param pika.connection.Connection connection: The closed connection obj
|
||||
:param int reply_code: The server provided reply_code if given
|
||||
:param str reply_text: The server provided reply_text if given
|
||||
|
||||
"""
|
||||
self._channel = None
|
||||
if self._stopping:
|
||||
self._connection.ioloop.stop()
|
||||
else:
|
||||
LOGGER.warning('Connection closed, reopening in 5 seconds: (%s) %s',
|
||||
reply_code, reply_text)
|
||||
self._connection.add_timeout(5, self._connection.ioloop.stop)
|
||||
|
||||
def open_channel(self):
|
||||
"""This method will open a new channel with RabbitMQ by issuing the
|
||||
Channel.Open RPC command. When RabbitMQ confirms the channel is open
|
||||
by sending the Channel.OpenOK RPC reply, the on_channel_open method
|
||||
will be invoked.
|
||||
|
||||
"""
|
||||
LOGGER.info('Creating a new channel')
|
||||
self._connection.channel(on_open_callback=self.on_channel_open)
|
||||
|
||||
def on_channel_open(self, channel):
|
||||
"""This method is invoked by pika when the channel has been opened.
|
||||
The channel object is passed in so we can make use of it.
|
||||
|
||||
Since the channel is now open, we'll declare the exchange to use.
|
||||
|
||||
:param pika.channel.Channel channel: The channel object
|
||||
|
||||
"""
|
||||
LOGGER.info('Channel opened')
|
||||
self._channel = channel
|
||||
self.add_on_channel_close_callback()
|
||||
self.setup_exchange(self.EXCHANGE)
|
||||
|
||||
def add_on_channel_close_callback(self):
|
||||
"""This method tells pika to call the on_channel_closed method if
|
||||
RabbitMQ unexpectedly closes the channel.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding channel close callback')
|
||||
self._channel.add_on_close_callback(self.on_channel_closed)
|
||||
|
||||
def on_channel_closed(self, channel, reply_code, reply_text):
|
||||
"""Invoked by pika when RabbitMQ unexpectedly closes the channel.
|
||||
Channels are usually closed if you attempt to do something that
|
||||
violates the protocol, such as re-declare an exchange or queue with
|
||||
different parameters. In this case, we'll close the connection
|
||||
to shutdown the object.
|
||||
|
||||
:param pika.channel.Channel channel: The closed channel
|
||||
:param int reply_code: The numeric reason the channel was closed
|
||||
:param str reply_text: The text reason the channel was closed
|
||||
|
||||
"""
|
||||
LOGGER.warning('Channel was closed: (%s) %s', reply_code, reply_text)
|
||||
self._channel = None
|
||||
if not self._stopping:
|
||||
self._connection.close()
|
||||
|
||||
def setup_exchange(self, exchange_name):
|
||||
"""Setup the exchange on RabbitMQ by invoking the Exchange.Declare RPC
|
||||
command. When it is complete, the on_exchange_declareok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param str|unicode exchange_name: The name of the exchange to declare
|
||||
|
||||
"""
|
||||
LOGGER.info('Declaring exchange %s', exchange_name)
|
||||
self._channel.exchange_declare(self.on_exchange_declareok,
|
||||
exchange_name,
|
||||
self.EXCHANGE_TYPE)
|
||||
|
||||
def on_exchange_declareok(self, unused_frame):
|
||||
"""Invoked by pika when RabbitMQ has finished the Exchange.Declare RPC
|
||||
command.
|
||||
|
||||
:param pika.Frame.Method unused_frame: Exchange.DeclareOk response frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Exchange declared')
|
||||
self.setup_queue(self.QUEUE)
|
||||
|
||||
def setup_queue(self, queue_name):
|
||||
"""Setup the queue on RabbitMQ by invoking the Queue.Declare RPC
|
||||
command. When it is complete, the on_queue_declareok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param str|unicode queue_name: The name of the queue to declare.
|
||||
|
||||
"""
|
||||
LOGGER.info('Declaring queue %s', queue_name)
|
||||
self._channel.queue_declare(self.on_queue_declareok, queue_name)
|
||||
|
||||
def on_queue_declareok(self, method_frame):
|
||||
"""Method invoked by pika when the Queue.Declare RPC call made in
|
||||
setup_queue has completed. In this method we will bind the queue
|
||||
and exchange together with the routing key by issuing the Queue.Bind
|
||||
RPC command. When this command is complete, the on_bindok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param pika.frame.Method method_frame: The Queue.DeclareOk frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Binding %s to %s with %s',
|
||||
self.EXCHANGE, self.QUEUE, self.ROUTING_KEY)
|
||||
self._channel.queue_bind(self.on_bindok, self.QUEUE,
|
||||
self.EXCHANGE, self.ROUTING_KEY)
|
||||
|
||||
def on_bindok(self, unused_frame):
|
||||
"""This method is invoked by pika when it receives the Queue.BindOk
|
||||
response from RabbitMQ. Since we know we're now setup and bound, it's
|
||||
time to start publishing."""
|
||||
LOGGER.info('Queue bound')
|
||||
self.start_publishing()
|
||||
|
||||
def start_publishing(self):
|
||||
"""This method will enable delivery confirmations and schedule the
|
||||
first message to be sent to RabbitMQ
|
||||
|
||||
"""
|
||||
LOGGER.info('Issuing consumer related RPC commands')
|
||||
self.enable_delivery_confirmations()
|
||||
self.schedule_next_message()
|
||||
|
||||
def enable_delivery_confirmations(self):
|
||||
"""Send the Confirm.Select RPC method to RabbitMQ to enable delivery
|
||||
confirmations on the channel. The only way to turn this off is to close
|
||||
the channel and create a new one.
|
||||
|
||||
When the message is confirmed from RabbitMQ, the
|
||||
on_delivery_confirmation method will be invoked passing in a Basic.Ack
|
||||
or Basic.Nack method from RabbitMQ that will indicate which messages it
|
||||
is confirming or rejecting.
|
||||
|
||||
"""
|
||||
LOGGER.info('Issuing Confirm.Select RPC command')
|
||||
self._channel.confirm_delivery(self.on_delivery_confirmation)
|
||||
|
||||
def on_delivery_confirmation(self, method_frame):
|
||||
"""Invoked by pika when RabbitMQ responds to a Basic.Publish RPC
|
||||
command, passing in either a Basic.Ack or Basic.Nack frame with
|
||||
the delivery tag of the message that was published. The delivery tag
|
||||
is an integer counter indicating the message number that was sent
|
||||
on the channel via Basic.Publish. Here we're just doing house keeping
|
||||
to keep track of stats and remove message numbers that we expect
|
||||
a delivery confirmation of from the list used to keep track of messages
|
||||
that are pending confirmation.
|
||||
|
||||
:param pika.frame.Method method_frame: Basic.Ack or Basic.Nack frame
|
||||
|
||||
"""
|
||||
confirmation_type = method_frame.method.NAME.split('.')[1].lower()
|
||||
LOGGER.info('Received %s for delivery tag: %i',
|
||||
confirmation_type,
|
||||
method_frame.method.delivery_tag)
|
||||
if confirmation_type == 'ack':
|
||||
self._acked += 1
|
||||
elif confirmation_type == 'nack':
|
||||
self._nacked += 1
|
||||
self._deliveries.remove(method_frame.method.delivery_tag)
|
||||
LOGGER.info('Published %i messages, %i have yet to be confirmed, '
|
||||
'%i were acked and %i were nacked',
|
||||
self._message_number, len(self._deliveries),
|
||||
self._acked, self._nacked)
|
||||
|
||||
def schedule_next_message(self):
|
||||
"""If we are not closing our connection to RabbitMQ, schedule another
|
||||
message to be delivered in PUBLISH_INTERVAL seconds.
|
||||
|
||||
"""
|
||||
LOGGER.info('Scheduling next message for %0.1f seconds',
|
||||
self.PUBLISH_INTERVAL)
|
||||
self._connection.add_timeout(self.PUBLISH_INTERVAL,
|
||||
self.publish_message)
|
||||
|
||||
def publish_message(self):
|
||||
"""If the class is not stopping, publish a message to RabbitMQ,
|
||||
appending a list of deliveries with the message number that was sent.
|
||||
This list will be used to check for delivery confirmations in the
|
||||
on_delivery_confirmations method.
|
||||
|
||||
Once the message has been sent, schedule another message to be sent.
|
||||
The main reason I put scheduling in was just so you can get a good idea
|
||||
of how the process is flowing by slowing down and speeding up the
|
||||
delivery intervals by changing the PUBLISH_INTERVAL constant in the
|
||||
class.
|
||||
|
||||
"""
|
||||
if self._channel is None or not self._channel.is_open:
|
||||
return
|
||||
|
||||
hdrs = {u'مفتاح': u' قيمة',
|
||||
u'键': u'值',
|
||||
u'キー': u'値'}
|
||||
properties = pika.BasicProperties(app_id='example-publisher',
|
||||
content_type='application/json',
|
||||
headers=hdrs)
|
||||
|
||||
message = u'مفتاح قيمة 键 值 キー 値'
|
||||
self._channel.basic_publish(self.EXCHANGE, self.ROUTING_KEY,
|
||||
json.dumps(message, ensure_ascii=False),
|
||||
properties)
|
||||
self._message_number += 1
|
||||
self._deliveries.append(self._message_number)
|
||||
LOGGER.info('Published message # %i', self._message_number)
|
||||
self.schedule_next_message()
|
||||
|
||||
def run(self):
|
||||
"""Run the example code by connecting and then starting the IOLoop.
|
||||
|
||||
"""
|
||||
while not self._stopping:
|
||||
self._connection = None
|
||||
self._deliveries = []
|
||||
self._acked = 0
|
||||
self._nacked = 0
|
||||
self._message_number = 0
|
||||
|
||||
try:
|
||||
self._connection = self.connect()
|
||||
self._connection.ioloop.start()
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
if (self._connection is not None and
|
||||
not self._connection.is_closed):
|
||||
# Finish closing
|
||||
self._connection.ioloop.start()
|
||||
|
||||
LOGGER.info('Stopped')
|
||||
|
||||
def stop(self):
|
||||
"""Stop the example by closing the channel and connection. We
|
||||
set a flag here so that we stop scheduling new messages to be
|
||||
published. The IOLoop is started because this method is
|
||||
invoked by the Try/Catch below when KeyboardInterrupt is caught.
|
||||
Starting the IOLoop again will allow the publisher to cleanly
|
||||
disconnect from RabbitMQ.
|
||||
|
||||
"""
|
||||
LOGGER.info('Stopping')
|
||||
self._stopping = True
|
||||
self.close_channel()
|
||||
self.close_connection()
|
||||
|
||||
def close_channel(self):
|
||||
"""Invoke this command to close the channel with RabbitMQ by sending
|
||||
the Channel.Close RPC command.
|
||||
|
||||
"""
|
||||
if self._channel is not None:
|
||||
LOGGER.info('Closing the channel')
|
||||
self._channel.close()
|
||||
|
||||
def close_connection(self):
|
||||
"""This method closes the connection to RabbitMQ."""
|
||||
if self._connection is not None:
|
||||
LOGGER.info('Closing connection')
|
||||
self._connection.close()
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT)
|
||||
|
||||
# Connect to localhost:5672 as guest with the password guest and virtual host "/" (%2F)
|
||||
example = ExamplePublisher('amqp://guest:guest@localhost:5672/%2F?connection_attempts=3&heartbeat_interval=3600')
|
||||
example.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,355 @@
|
|||
Asyncio Consumer
|
||||
================
|
||||
The following example implements a consumer using the
|
||||
:class:`Asyncio adapter <pika.adapters.asyncio_connection.AsyncioConnection>` for the
|
||||
`Asyncio library <https://docs.python.org/3/library/asyncio.html>`_ that will respond to RPC commands sent
|
||||
from RabbitMQ. For example, it will reconnect if RabbitMQ closes the connection and will shutdown if
|
||||
RabbitMQ cancels the consumer or closes the channel. While it may look intimidating, each method is
|
||||
very short and represents a individual actions that a consumer can do.
|
||||
|
||||
consumer.py::
|
||||
|
||||
from pika import adapters
|
||||
import pika
|
||||
import logging
|
||||
|
||||
LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) '
|
||||
'-35s %(lineno) -5d: %(message)s')
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExampleConsumer(object):
|
||||
"""This is an example consumer that will handle unexpected interactions
|
||||
with RabbitMQ such as channel and connection closures.
|
||||
|
||||
If RabbitMQ closes the connection, it will reopen it. You should
|
||||
look at the output, as there are limited reasons why the connection may
|
||||
be closed, which usually are tied to permission related issues or
|
||||
socket timeouts.
|
||||
|
||||
If the channel is closed, it will indicate a problem with one of the
|
||||
commands that were issued and that should surface in the output as well.
|
||||
|
||||
"""
|
||||
EXCHANGE = 'message'
|
||||
EXCHANGE_TYPE = 'topic'
|
||||
QUEUE = 'text'
|
||||
ROUTING_KEY = 'example.text'
|
||||
|
||||
def __init__(self, amqp_url):
|
||||
"""Create a new instance of the consumer class, passing in the AMQP
|
||||
URL used to connect to RabbitMQ.
|
||||
|
||||
:param str amqp_url: The AMQP url to connect with
|
||||
|
||||
"""
|
||||
self._connection = None
|
||||
self._channel = None
|
||||
self._closing = False
|
||||
self._consumer_tag = None
|
||||
self._url = amqp_url
|
||||
|
||||
def connect(self):
|
||||
"""This method connects to RabbitMQ, returning the connection handle.
|
||||
When the connection is established, the on_connection_open method
|
||||
will be invoked by pika.
|
||||
|
||||
:rtype: pika.SelectConnection
|
||||
|
||||
"""
|
||||
LOGGER.info('Connecting to %s', self._url)
|
||||
return adapters.asyncio_connection.AsyncioConnection(pika.URLParameters(self._url),
|
||||
self.on_connection_open)
|
||||
|
||||
def close_connection(self):
|
||||
"""This method closes the connection to RabbitMQ."""
|
||||
LOGGER.info('Closing connection')
|
||||
self._connection.close()
|
||||
|
||||
def add_on_connection_close_callback(self):
|
||||
"""This method adds an on close callback that will be invoked by pika
|
||||
when RabbitMQ closes the connection to the publisher unexpectedly.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding connection close callback')
|
||||
self._connection.add_on_close_callback(self.on_connection_closed)
|
||||
|
||||
def on_connection_closed(self, connection, reply_code, reply_text):
|
||||
"""This method is invoked by pika when the connection to RabbitMQ is
|
||||
closed unexpectedly. Since it is unexpected, we will reconnect to
|
||||
RabbitMQ if it disconnects.
|
||||
|
||||
:param pika.connection.Connection connection: The closed connection obj
|
||||
:param int reply_code: The server provided reply_code if given
|
||||
:param str reply_text: The server provided reply_text if given
|
||||
|
||||
"""
|
||||
self._channel = None
|
||||
if self._closing:
|
||||
self._connection.ioloop.stop()
|
||||
else:
|
||||
LOGGER.warning('Connection closed, reopening in 5 seconds: (%s) %s',
|
||||
reply_code, reply_text)
|
||||
self._connection.add_timeout(5, self.reconnect)
|
||||
|
||||
def on_connection_open(self, unused_connection):
|
||||
"""This method is called by pika once the connection to RabbitMQ has
|
||||
been established. It passes the handle to the connection object in
|
||||
case we need it, but in this case, we'll just mark it unused.
|
||||
|
||||
:type unused_connection: pika.SelectConnection
|
||||
|
||||
"""
|
||||
LOGGER.info('Connection opened')
|
||||
self.add_on_connection_close_callback()
|
||||
self.open_channel()
|
||||
|
||||
def reconnect(self):
|
||||
"""Will be invoked by the IOLoop timer if the connection is
|
||||
closed. See the on_connection_closed method.
|
||||
|
||||
"""
|
||||
if not self._closing:
|
||||
|
||||
# Create a new connection
|
||||
self._connection = self.connect()
|
||||
|
||||
def add_on_channel_close_callback(self):
|
||||
"""This method tells pika to call the on_channel_closed method if
|
||||
RabbitMQ unexpectedly closes the channel.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding channel close callback')
|
||||
self._channel.add_on_close_callback(self.on_channel_closed)
|
||||
|
||||
def on_channel_closed(self, channel, reply_code, reply_text):
|
||||
"""Invoked by pika when RabbitMQ unexpectedly closes the channel.
|
||||
Channels are usually closed if you attempt to do something that
|
||||
violates the protocol, such as re-declare an exchange or queue with
|
||||
different parameters. In this case, we'll close the connection
|
||||
to shutdown the object.
|
||||
|
||||
:param pika.channel.Channel: The closed channel
|
||||
:param int reply_code: The numeric reason the channel was closed
|
||||
:param str reply_text: The text reason the channel was closed
|
||||
|
||||
"""
|
||||
LOGGER.warning('Channel %i was closed: (%s) %s',
|
||||
channel, reply_code, reply_text)
|
||||
self._connection.close()
|
||||
|
||||
def on_channel_open(self, channel):
|
||||
"""This method is invoked by pika when the channel has been opened.
|
||||
The channel object is passed in so we can make use of it.
|
||||
|
||||
Since the channel is now open, we'll declare the exchange to use.
|
||||
|
||||
:param pika.channel.Channel channel: The channel object
|
||||
|
||||
"""
|
||||
LOGGER.info('Channel opened')
|
||||
self._channel = channel
|
||||
self.add_on_channel_close_callback()
|
||||
self.setup_exchange(self.EXCHANGE)
|
||||
|
||||
def setup_exchange(self, exchange_name):
|
||||
"""Setup the exchange on RabbitMQ by invoking the Exchange.Declare RPC
|
||||
command. When it is complete, the on_exchange_declareok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param str|unicode exchange_name: The name of the exchange to declare
|
||||
|
||||
"""
|
||||
LOGGER.info('Declaring exchange %s', exchange_name)
|
||||
self._channel.exchange_declare(self.on_exchange_declareok,
|
||||
exchange_name,
|
||||
self.EXCHANGE_TYPE)
|
||||
|
||||
def on_exchange_declareok(self, unused_frame):
|
||||
"""Invoked by pika when RabbitMQ has finished the Exchange.Declare RPC
|
||||
command.
|
||||
|
||||
:param pika.Frame.Method unused_frame: Exchange.DeclareOk response frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Exchange declared')
|
||||
self.setup_queue(self.QUEUE)
|
||||
|
||||
def setup_queue(self, queue_name):
|
||||
"""Setup the queue on RabbitMQ by invoking the Queue.Declare RPC
|
||||
command. When it is complete, the on_queue_declareok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param str|unicode queue_name: The name of the queue to declare.
|
||||
|
||||
"""
|
||||
LOGGER.info('Declaring queue %s', queue_name)
|
||||
self._channel.queue_declare(self.on_queue_declareok, queue_name)
|
||||
|
||||
def on_queue_declareok(self, method_frame):
|
||||
"""Method invoked by pika when the Queue.Declare RPC call made in
|
||||
setup_queue has completed. In this method we will bind the queue
|
||||
and exchange together with the routing key by issuing the Queue.Bind
|
||||
RPC command. When this command is complete, the on_bindok method will
|
||||
be invoked by pika.
|
||||
|
||||
:param pika.frame.Method method_frame: The Queue.DeclareOk frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Binding %s to %s with %s',
|
||||
self.EXCHANGE, self.QUEUE, self.ROUTING_KEY)
|
||||
self._channel.queue_bind(self.on_bindok, self.QUEUE,
|
||||
self.EXCHANGE, self.ROUTING_KEY)
|
||||
|
||||
def add_on_cancel_callback(self):
|
||||
"""Add a callback that will be invoked if RabbitMQ cancels the consumer
|
||||
for some reason. If RabbitMQ does cancel the consumer,
|
||||
on_consumer_cancelled will be invoked by pika.
|
||||
|
||||
"""
|
||||
LOGGER.info('Adding consumer cancellation callback')
|
||||
self._channel.add_on_cancel_callback(self.on_consumer_cancelled)
|
||||
|
||||
def on_consumer_cancelled(self, method_frame):
|
||||
"""Invoked by pika when RabbitMQ sends a Basic.Cancel for a consumer
|
||||
receiving messages.
|
||||
|
||||
:param pika.frame.Method method_frame: The Basic.Cancel frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Consumer was cancelled remotely, shutting down: %r',
|
||||
method_frame)
|
||||
if self._channel:
|
||||
self._channel.close()
|
||||
|
||||
def acknowledge_message(self, delivery_tag):
|
||||
"""Acknowledge the message delivery from RabbitMQ by sending a
|
||||
Basic.Ack RPC method for the delivery tag.
|
||||
|
||||
:param int delivery_tag: The delivery tag from the Basic.Deliver frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Acknowledging message %s', delivery_tag)
|
||||
self._channel.basic_ack(delivery_tag)
|
||||
|
||||
def on_message(self, unused_channel, basic_deliver, properties, body):
|
||||
"""Invoked by pika when a message is delivered from RabbitMQ. The
|
||||
channel is passed for your convenience. The basic_deliver object that
|
||||
is passed in carries the exchange, routing key, delivery tag and
|
||||
a redelivered flag for the message. The properties passed in is an
|
||||
instance of BasicProperties with the message properties and the body
|
||||
is the message that was sent.
|
||||
|
||||
:param pika.channel.Channel unused_channel: The channel object
|
||||
:param pika.Spec.Basic.Deliver: basic_deliver method
|
||||
:param pika.Spec.BasicProperties: properties
|
||||
:param str|unicode body: The message body
|
||||
|
||||
"""
|
||||
LOGGER.info('Received message # %s from %s: %s',
|
||||
basic_deliver.delivery_tag, properties.app_id, body)
|
||||
self.acknowledge_message(basic_deliver.delivery_tag)
|
||||
|
||||
def on_cancelok(self, unused_frame):
|
||||
"""This method is invoked by pika when RabbitMQ acknowledges the
|
||||
cancellation of a consumer. At this point we will close the channel.
|
||||
This will invoke the on_channel_closed method once the channel has been
|
||||
closed, which will in-turn close the connection.
|
||||
|
||||
:param pika.frame.Method unused_frame: The Basic.CancelOk frame
|
||||
|
||||
"""
|
||||
LOGGER.info('RabbitMQ acknowledged the cancellation of the consumer')
|
||||
self.close_channel()
|
||||
|
||||
def stop_consuming(self):
|
||||
"""Tell RabbitMQ that you would like to stop consuming by sending the
|
||||
Basic.Cancel RPC command.
|
||||
|
||||
"""
|
||||
if self._channel:
|
||||
LOGGER.info('Sending a Basic.Cancel RPC command to RabbitMQ')
|
||||
self._channel.basic_cancel(self.on_cancelok, self._consumer_tag)
|
||||
|
||||
def start_consuming(self):
|
||||
"""This method sets up the consumer by first calling
|
||||
add_on_cancel_callback so that the object is notified if RabbitMQ
|
||||
cancels the consumer. It then issues the Basic.Consume RPC command
|
||||
which returns the consumer tag that is used to uniquely identify the
|
||||
consumer with RabbitMQ. We keep the value to use it when we want to
|
||||
cancel consuming. The on_message method is passed in as a callback pika
|
||||
will invoke when a message is fully received.
|
||||
|
||||
"""
|
||||
LOGGER.info('Issuing consumer related RPC commands')
|
||||
self.add_on_cancel_callback()
|
||||
self._consumer_tag = self._channel.basic_consume(self.on_message,
|
||||
self.QUEUE)
|
||||
|
||||
def on_bindok(self, unused_frame):
|
||||
"""Invoked by pika when the Queue.Bind method has completed. At this
|
||||
point we will start consuming messages by calling start_consuming
|
||||
which will invoke the needed RPC commands to start the process.
|
||||
|
||||
:param pika.frame.Method unused_frame: The Queue.BindOk response frame
|
||||
|
||||
"""
|
||||
LOGGER.info('Queue bound')
|
||||
self.start_consuming()
|
||||
|
||||
def close_channel(self):
|
||||
"""Call to close the channel with RabbitMQ cleanly by issuing the
|
||||
Channel.Close RPC command.
|
||||
|
||||
"""
|
||||
LOGGER.info('Closing the channel')
|
||||
self._channel.close()
|
||||
|
||||
def open_channel(self):
|
||||
"""Open a new channel with RabbitMQ by issuing the Channel.Open RPC
|
||||
command. When RabbitMQ responds that the channel is open, the
|
||||
on_channel_open callback will be invoked by pika.
|
||||
|
||||
"""
|
||||
LOGGER.info('Creating a new channel')
|
||||
self._connection.channel(on_open_callback=self.on_channel_open)
|
||||
|
||||
def run(self):
|
||||
"""Run the example consumer by connecting to RabbitMQ and then
|
||||
starting the IOLoop to block and allow the SelectConnection to operate.
|
||||
|
||||
"""
|
||||
self._connection = self.connect()
|
||||
self._connection.ioloop.start()
|
||||
|
||||
def stop(self):
|
||||
"""Cleanly shutdown the connection to RabbitMQ by stopping the consumer
|
||||
with RabbitMQ. When RabbitMQ confirms the cancellation, on_cancelok
|
||||
will be invoked by pika, which will then closing the channel and
|
||||
connection. The IOLoop is started again because this method is invoked
|
||||
when CTRL-C is pressed raising a KeyboardInterrupt exception. This
|
||||
exception stops the IOLoop which needs to be running for pika to
|
||||
communicate with RabbitMQ. All of the commands issued prior to starting
|
||||
the IOLoop will be buffered but not processed.
|
||||
|
||||
"""
|
||||
LOGGER.info('Stopping')
|
||||
self._closing = True
|
||||
self.stop_consuming()
|
||||
self._connection.ioloop.start()
|
||||
LOGGER.info('Stopped')
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
|
||||
example = ExampleConsumer('amqp://guest:guest@localhost:5672/%2F')
|
||||
try:
|
||||
example.run()
|
||||
except KeyboardInterrupt:
|
||||
example.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Using the Blocking Connection to get a message from RabbitMQ
|
||||
============================================================
|
||||
|
||||
.. _example_blocking_basic_get:
|
||||
|
||||
The :py:meth:`BlockingChannel.basic_get <pika.adapters.blocking_connection.BlockingChannel.basic_get>` method will return a tuple with the members.
|
||||
|
||||
If the server returns a message, the first item in the tuple will be a :class:`pika.spec.Basic.GetOk` object with the current message count, the redelivered flag, the routing key that was used to put the message in the queue, and the exchange the message was published to. The second item will be a :py:class:`~pika.spec.BasicProperties` object and the third will be the message body.
|
||||
|
||||
If the server did not return a message a tuple of None, None, None will be returned.
|
||||
|
||||
Example of getting a message and acknowledging it::
|
||||
|
||||
import pika
|
||||
|
||||
connection = pika.BlockingConnection()
|
||||
channel = connection.channel()
|
||||
method_frame, header_frame, body = channel.basic_get('test')
|
||||
if method_frame:
|
||||
print(method_frame, header_frame, body)
|
||||
channel.basic_ack(method_frame.delivery_tag)
|
||||
else:
|
||||
print('No message returned')
|
|
@ -0,0 +1,29 @@
|
|||
Using the Blocking Connection to consume messages from RabbitMQ
|
||||
===============================================================
|
||||
|
||||
.. _example_blocking_basic_consume:
|
||||
|
||||
The :py:meth:`BlockingChannel.basic_consume <pika.adapters.blocking_connection.BlockingChannel.basic_consume>` method assign a callback method to be called every time that RabbitMQ delivers messages to your consuming application.
|
||||
|
||||
When pika calls your method, it will pass in the channel, a :py:class:`pika.spec.Basic.Deliver` object with the delivery tag, the redelivered flag, the routing key that was used to put the message in the queue, and the exchange the message was published to. The third argument will be a :py:class:`pika.spec.BasicProperties` object and the last will be the message body.
|
||||
|
||||
Example of consuming messages and acknowledging them::
|
||||
|
||||
import pika
|
||||
|
||||
|
||||
def on_message(channel, method_frame, header_frame, body):
|
||||
print(method_frame.delivery_tag)
|
||||
print(body)
|
||||
print()
|
||||
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
|
||||
|
||||
|
||||
connection = pika.BlockingConnection()
|
||||
channel = connection.channel()
|
||||
channel.basic_consume(on_message, 'test')
|
||||
try:
|
||||
channel.start_consuming()
|
||||
except KeyboardInterrupt:
|
||||
channel.stop_consuming()
|
||||
connection.close()
|
|
@ -0,0 +1,73 @@
|
|||
Using the BlockingChannel.consume generator to consume messages
|
||||
===============================================================
|
||||
|
||||
.. _example_blocking_basic_get:
|
||||
|
||||
The :py:meth:`BlockingChannel.consume <pika.adapters.blocking_connection.BlockingChannel.consume>` method is a generator that will return a tuple of method, properties and body.
|
||||
|
||||
When you escape out of the loop, be sure to call consumer.cancel() to return any unprocessed messages.
|
||||
|
||||
Example of consuming messages and acknowledging them::
|
||||
|
||||
import pika
|
||||
|
||||
connection = pika.BlockingConnection()
|
||||
channel = connection.channel()
|
||||
|
||||
# Get ten messages and break out
|
||||
for method_frame, properties, body in channel.consume('test'):
|
||||
|
||||
# Display the message parts
|
||||
print(method_frame)
|
||||
print(properties)
|
||||
print(body)
|
||||
|
||||
# Acknowledge the message
|
||||
channel.basic_ack(method_frame.delivery_tag)
|
||||
|
||||
# Escape out of the loop after 10 messages
|
||||
if method_frame.delivery_tag == 10:
|
||||
break
|
||||
|
||||
# Cancel the consumer and return any pending messages
|
||||
requeued_messages = channel.cancel()
|
||||
print('Requeued %i messages' % requeued_messages)
|
||||
|
||||
# Close the channel and the connection
|
||||
channel.close()
|
||||
connection.close()
|
||||
|
||||
If you have pending messages in the test queue, your output should look something like::
|
||||
|
||||
(pika)gmr-0x02:pika gmr$ python blocking_nack.py
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=1', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=2', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=3', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=4', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=5', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=6', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=7', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=8', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=9', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
<Basic.Deliver(['consumer_tag=ctag1.0', 'redelivered=True', 'routing_key=test', 'delivery_tag=10', 'exchange=test'])>
|
||||
<BasicProperties(['delivery_mode=1', 'content_type=text/plain'])>
|
||||
Hello World!
|
||||
Requeued 1894 messages
|
|
@ -0,0 +1,28 @@
|
|||
Using Delivery Confirmations with the BlockingConnection
|
||||
========================================================
|
||||
|
||||
The following code demonstrates how to turn on delivery confirmations with the BlockingConnection and how to check for confirmation from RabbitMQ::
|
||||
|
||||
import pika
|
||||
|
||||
# Open a connection to RabbitMQ on localhost using all default parameters
|
||||
connection = pika.BlockingConnection()
|
||||
|
||||
# Open the channel
|
||||
channel = connection.channel()
|
||||
|
||||
# Declare the queue
|
||||
channel.queue_declare(queue="test", durable=True, exclusive=False, auto_delete=False)
|
||||
|
||||
# Turn on delivery confirmations
|
||||
channel.confirm_delivery()
|
||||
|
||||
# Send a message
|
||||
if channel.basic_publish(exchange='test',
|
||||
routing_key='test',
|
||||
body='Hello World!',
|
||||
properties=pika.BasicProperties(content_type='text/plain',
|
||||
delivery_mode=1)):
|
||||
print('Message publish was confirmed')
|
||||
else:
|
||||
print('Message could not be confirmed')
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue