diff --git a/.vscode/settings.json b/.vscode/settings.json
index 41b6fdb..01b6ec9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -54,5 +54,12 @@
"editor.quickSuggestions": {
"other": "off"
},
- "editor.snippetSuggestions": "none"
+ "editor.snippetSuggestions": "none",
+ "python.analysis.extraPaths": [
+ "${workspaceFolder}/modules",
+ "${workspaceFolder}/modules/lib",
+ "${workspaceFolder}/modules/simul",
+ "${workspaceFolder}/modules/lib/tools",
+ "${workspaceFolder}/modules/lib/shell"
+ ]
}
\ No newline at end of file
diff --git a/build.py b/build.py
index 6a550c2..26ddf75 100755
--- a/build.py
+++ b/build.py
@@ -18,8 +18,11 @@
# Mov to gif :
# ffmpeg -i video.mov -vf "fps=3,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 output.gif
# 640x360
+#
+# Vmware share folder add next lines in /etc/fstab :
+# vmhgfs-fuse /mnt/hgfs fuse defaults,allow_other 0 0
-MICROPYTHON_VERSION ="f4811b0b42f10aa12fc2f94c0459344d84c89eb8"#"12f99481518b0ebcb14f00b2323865a845c2a4f1"
+MICROPYTHON_VERSION ="f4811b0b42f10aa12fc2f94c0459344d84c89eb8"
ESP_IDF_VERSION ="-b v4.4.2"
ESP32_CAMERA_VERSION="722497cb19383cd4ee6b5d57bb73148b5af41b24" # Very stable version but cannot be rebuild with chip esp32s3
ESP32_CAMERA_VERSION_S3="5c8349f4cf169c8a61283e0da9b8cff10994d3f3" # Reliability problem but Esp32 S3 firmware can build with it
diff --git a/doc/CAMFLASHER.md b/doc/CAMFLASHER.md
index c92ef1f..e0d3757 100644
--- a/doc/CAMFLASHER.md
+++ b/doc/CAMFLASHER.md
@@ -16,40 +16,33 @@ Camflasher with shell commands **upload** and **download**, allows to upload or
- [CP210 drivers](https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers)
- [FTDI drivers](https://ftdichip.com/drivers/vcp-drivers/)
-
- Download the firmware associated with your device :
- - [ESP32-CAM, ESP32-CAM-MB, ESP32ONE, M5Stack Camera](https://github.com/remibert/pycameresp/releases/download/V13/ESP32CAM-firmware.bin)
-
- - [ESP32-NODEMCU, LOLIN32, ESP32 without SPIRAM](https://github.com/remibert/pycameresp/releases/download/V13/GENERIC-firmware.bin)
+ - [ESP32-CAM, ESP32-CAM-MB, ESP32ONE, M5Stack Camera](https://github.com/remibert/pycameresp/releases/download/V14/ESP32CAM-firmware.bin)
- - [ESP32-TTGO-T8 or ESP32 with SPIRAM ](https://github.com/remibert/pycameresp/releases/download/V13/GENERIC_SPIRAM-firmware.bin)
+ - [ESP32-TTGO-T8 or ESP32 with SPIRAM ](https://github.com/remibert/pycameresp/releases/download/V14/GENERIC_SPIRAM-firmware.bin)
- - [Pico PI W](https://github.com/remibert/pycameresp/releases/download/V13/PICO_W-firmware.uf2)
- Or download the zip for standard micropython firmware :
- - [Shell with editor for RP2 pico Pi](https://github.com/remibert/pycameresp/releases/download/V13/shell.zip)
+ - [Shell with editor](https://github.com/remibert/pycameresp/releases/download/V14/shell.zip)
- - [Wifi manager, Http server](https://github.com/remibert/pycameresp/releases/download/V13/server.zip)
+ - [Wifi manager, Http server](https://github.com/remibert/pycameresp/releases/download/V14/server.zip)
- Text editor source files running on python3 and micropython :
- - [Text editor](https://github.com/remibert/pycameresp/releases/download/V13/editor.zip)
+ - [Text editor](https://github.com/remibert/pycameresp/releases/download/V14/editor.zip)
It is possible to run the shell with editor, or the servers on a standard micropython platform. The servers, the wifi manager, requires having enough ram and wifi support (ESP32 with SPIRAM for example). Unzip archive and install it with rshell.
- Download the camflasher application and unzip it :
- - [CamFlasher for Windows 10 64 bits](https://github.com/remibert/pycameresp/releases/download/V13/CamFlasher_windows_10_64.zip)
-
- - [CamFlasher for Windows seven 64 bits](https://github.com/remibert/pycameresp/releases/download/V13/CamFlasher_windows_7_64.zip)
-
- - [CamFlasher for OSX Big Sur Intel](https://github.com/remibert/pycameresp/releases/download/V13/CamFlasher_osx.zip)
+ - [CamFlasher for Windows 10 64 bits](https://github.com/remibert/pycameresp/releases/download/V14/CamFlasher_windows_10_64.zip)
- - [CamFlasher for Debian 11 x86_64](https://github.com/remibert/pycameresp/releases/download/V13/CamFlasher_linux.zip)
+ - [CamFlasher for OSX Big Sur Intel](https://github.com/remibert/pycameresp/releases/download/V14/CamFlasher_osx.zip)
+ - [CamFlasher for Debian 11 x86_64](https://github.com/remibert/pycameresp/releases/download/V14/CamFlasher_linux.zip)
- On linux to be able to operate without being a super user you must enter the commands :
diff --git a/doc/DIRECTORIES.md b/doc/DIRECTORIES.md
index 08f9557..7aa9032 100644
--- a/doc/DIRECTORIES.md
+++ b/doc/DIRECTORIES.md
@@ -18,6 +18,7 @@ Below are the directory details :
- **modules/lib/motion** : motion detection python sources
- **modules/lib/tools** : tools used for all python sources
- **modules/lib/server** : Ftp, Http, Pushover, Telnet, Ntp synchronization, user and session python sources
+- **modules/lib/electricmeter** : used to monitor the consumption of an electric meter (not present in the firmware)
- **modules/lib/wifi** : Wifi and accesspoint python sources
- **modules/config** : Configuration saved in this directory
diff --git a/doc/FIRMWARE.md b/doc/FIRMWARE.md
index 21cdf50..58e78e7 100644
--- a/doc/FIRMWARE.md
+++ b/doc/FIRMWARE.md
@@ -37,13 +37,12 @@ The first time use command (get source, install required software and build firm
And after juste for rebuild use command :
- **python3 build.py --patch --build "ESP32CAM"**
-Replace **ESP32CAM** by your prefered firmware, add double quote if you want to use wildcards for build many firmwares, for example ESP32 GENERIC :
-- **python3 build.py --patch --build "GENERIC"**
+Replace **ESP32CAM** by your prefered firmware, add double quote if you want to use wildcards for build many firmwares, for example ESP32 GENERIC SPIRAM :
- **python3 build.py --patch --build "GENERIC_SPIRAM"**
Or for all ESP32 S2
-- **python3 build.py --patch --build "ESP32CAM" "GENERIC" "GENERIC_SPIRAM"**
+- **python3 build.py --patch --build "ESP32CAM" "GENERIC_SPIRAM"**
To build ESP32 S3 you must clean all and add option --s3 :
-- **python3 build.py --clean --s3 --patch --build "GENERIC_S3_*"**
+- **python3 build.py --clean --s3 --patch --build "GENERIC_S3_SPIRAM"**
diff --git a/doc/REQUIREMENTS.md b/doc/REQUIREMENTS.md
index 61e8891..b58ca5f 100644
--- a/doc/REQUIREMENTS.md
+++ b/doc/REQUIREMENTS.md
@@ -15,7 +15,9 @@ Below are the devices compatible with pycameresp :
![ESP32CAM](/images/Device_ESP32CAM.jpg "ESP32CAM")
![ESP32CAM-MB](/images/Device_ESP32CAM-MB.jpg "ESP32CAM-MB")
-![NODEMCU](/images/Device_NODEMCU.jpg "NODE MCU") ![LOLIN32](/images/Device_LOLIN32.jpg "LOLIN32")
![TTGO](/images/Device_TTGO.jpg "TTGO")
![ESP32ONE](/images/Device_ESP32ONE.jpg "ESP32ONE")
-![M5StackCamera](/images/Device_M5StackCamera.jpg "M5StackCamera")
\ No newline at end of file
+![M5StackCamera](/images/Device_M5StackCamera.jpg "M5StackCamera")
+![BPI-Leaf-S3](/images/Device_BPI-Leaf-S3.png "BPI-Leaf-S3")
+
+**Devices without spiram have been removed, the platform can work, but we often fall into a lack of memory.**
\ No newline at end of file
diff --git a/doc/SCREENSHOTS.md b/doc/SCREENSHOTS.md
index 84dc4ab..f7b5312 100644
--- a/doc/SCREENSHOTS.md
+++ b/doc/SCREENSHOTS.md
@@ -4,7 +4,11 @@
The web interface allows configuration of the network, servers, etc... gives device information, and allows video streaming.
-![WebInterface.gif](/images/WebInterface.gif "Board information web page")
+![WebInterface.gif](/images/WebInterface.gif "Web pages")
+
+Responsive interface for smartphones and tablets
+
+![ResponsiceWebInterface.gif](/images/ResponsiveWebInterface.gif "Responsive Web pages")
Smartphone motion detection notification (with pushover application)
diff --git a/doc/lib/electricmeter/config.html b/doc/lib/electricmeter/config.html
new file mode 100644
index 0000000..3f36ddc
--- /dev/null
+++ b/doc/lib/electricmeter/config.html
@@ -0,0 +1,837 @@
+
+
+
class RatesConfig(jsonconfig.JsonConfig):
+ """ Rates list per kwh """
+ config = None
+ def __init__(self):
+ """ Constructor """
+ jsonconfig.JsonConfig.__init__(self)
+ self.rates = []
+
+ def append(self, rate):
+ """ Add new rate in the list """
+ found = False
+ rate = strings.tobytes(rate)
+ for current in self.rates:
+ if current[b"name"] == strings.tobytes(rate.name) and current[b"validity_date"] == strings.tobytes(rate.validity_date):
+ found = True
+ current[b"currency"] = strings.tobytes(rate.currency)
+ current[b"price"] = rate.price
+ break
+ if found is False:
+ self.rates.append(strings.tobytes(rate.__dict__))
+
+ def get(self, index):
+ """ Return the rate at the index """
+ try:
+ return self.rates[int(index)]
+ except:
+ return None
+
+ def remove(self, index):
+ """ Remove the rate at the index """
+ try:
+ del self.rates[int(index)]
+ except:
+ pass
+
+ def search_rates(self, day):
+ """ Find the current rate """
+ result = {}
+ day = int(day)
+ for rate in self.rates:
+ # If the rate is valid for the current date
+ if day >= rate[b"validity_date"]:
+ # If the same rate already found
+ if rate[b"name"] in result:
+ # If the rate already found is older than the current rate
+ if result[rate[b"name"]][b"validity_date"] < rate[b"validity_date"]:
+ # Replace by the current rate
+ result[rate[b"name"]] = rate
+ else:
+ # Keep the current rate
+ result[rate[b"name"]] = rate
+ return result
+
+ @staticmethod
+ def get_config():
+ """ Return the singleton configuration """
+ if RatesConfig.config is None:
+ RatesConfig.config = RatesConfig()
+ RatesConfig.config.load()
+ return RatesConfig.config
+
+
Ancestors
+
+
tools.jsonconfig.JsonConfig
+
+
Class variables
+
+
var config
+
+
+
+
+
Static methods
+
+
+def get_config()
+
+
+
Return the singleton configuration
+
+
+Expand source code
+
+
@staticmethod
+def get_config():
+ """ Return the singleton configuration """
+ if RatesConfig.config is None:
+ RatesConfig.config = RatesConfig()
+ RatesConfig.config.load()
+ return RatesConfig.config
+
+
+
+
Methods
+
+
+def append(self, rate)
+
+
+
Add new rate in the list
+
+
+Expand source code
+
+
def append(self, rate):
+ """ Add new rate in the list """
+ found = False
+ rate = strings.tobytes(rate)
+ for current in self.rates:
+ if current[b"name"] == strings.tobytes(rate.name) and current[b"validity_date"] == strings.tobytes(rate.validity_date):
+ found = True
+ current[b"currency"] = strings.tobytes(rate.currency)
+ current[b"price"] = rate.price
+ break
+ if found is False:
+ self.rates.append(strings.tobytes(rate.__dict__))
+
+
+
+def get(self, index)
+
+
+
Return the rate at the index
+
+
+Expand source code
+
+
def get(self, index):
+ """ Return the rate at the index """
+ try:
+ return self.rates[int(index)]
+ except:
+ return None
+
+
+
+def remove(self, index)
+
+
+
Remove the rate at the index
+
+
+Expand source code
+
+
def remove(self, index):
+ """ Remove the rate at the index """
+ try:
+ del self.rates[int(index)]
+ except:
+ pass
+
+
+
+def search_rates(self, day)
+
+
+
Find the current rate
+
+
+Expand source code
+
+
def search_rates(self, day):
+ """ Find the current rate """
+ result = {}
+ day = int(day)
+ for rate in self.rates:
+ # If the rate is valid for the current date
+ if day >= rate[b"validity_date"]:
+ # If the same rate already found
+ if rate[b"name"] in result:
+ # If the rate already found is older than the current rate
+ if result[rate[b"name"]][b"validity_date"] < rate[b"validity_date"]:
+ # Replace by the current rate
+ result[rate[b"name"]] = rate
+ else:
+ # Keep the current rate
+ result[rate[b"name"]] = rate
+ return result
class TimeSlotsConfig(jsonconfig.JsonConfig):
+ """ Time slots list """
+ config = None
+ def __init__(self):
+ """ Constructor """
+ jsonconfig.JsonConfig.__init__(self)
+ self.time_slots = []
+
+ def append(self, time_slot):
+ """ Add new time slot in the list """
+ found = False
+ time_slot = strings.tobytes(time_slot)
+ for current in self.time_slots:
+ if current[b"start_time"] == time_slot.start_time and current[b"end_time"] == time_slot.end_time:
+ current[b"color"] = time_slot.color
+ current[b"rate"] = time_slot.rate
+ found = True
+ break
+ if found is False:
+ self.time_slots.append(strings.tobytes(time_slot.__dict__))
+
+ def get(self, index):
+ """ Return the time slot at the index """
+ try:
+ return self.time_slots[int(index)]
+ except:
+ return None
+
+ def remove(self, index):
+ """ Remove the time slot at the index """
+ try:
+ del self.time_slots[int(index)]
+ except:
+ pass
+
+ def get_prices(self, rates):
+ """ Return the list of prices according to the day """
+ if rates == {}:
+ result = [{b'rate': b'', b'start_time': 0, b'end_time': 86340, b'color': b'#5498e0', b'price': 0, b'currency': b'not initialized'}]
+ else:
+ result = self.time_slots[:]
+ for time_slot in result:
+ time_slot[b"price"] = rates[time_slot[b"rate"]][b"price"]
+ time_slot[b"currency"] = rates[time_slot[b"rate"]][b"currency"]
+ return result
+
+ @staticmethod
+ def get_config():
+ """ Return the singleton configuration """
+ if TimeSlotsConfig.config is None:
+ TimeSlotsConfig.config = TimeSlotsConfig()
+ TimeSlotsConfig.config.load()
+ return TimeSlotsConfig.config
+
+ @staticmethod
+ def get_cost(day):
+ """ Get the cost according to the day selected """
+ time_slots = TimeSlotsConfig.get_config()
+ rates = RatesConfig.get_config()
+ return time_slots.get_prices(rates.search_rates(day))
+
+ @staticmethod
+ def create_empty_slot(size):
+ """ Create empty time slot """
+ time_slots = TimeSlotsConfig()
+ time_slots.load()
+ slot_pulses = {}
+ index = 0
+ while True:
+ time_slot = time_slots.get(index)
+ if time_slot is None:
+ break
+ slot_pulses[(time_slot[b"start_time"], time_slot[b"end_time"])] = [0]*size
+ index += 1
+ if len(slot_pulses) == 0:
+ slot_pulses[(0,1439*60)] = [0]*size
+ return slot_pulses
+
+
Ancestors
+
+
tools.jsonconfig.JsonConfig
+
+
Class variables
+
+
var config
+
+
+
+
+
Static methods
+
+
+def create_empty_slot(size)
+
+
+
Create empty time slot
+
+
+Expand source code
+
+
@staticmethod
+def create_empty_slot(size):
+ """ Create empty time slot """
+ time_slots = TimeSlotsConfig()
+ time_slots.load()
+ slot_pulses = {}
+ index = 0
+ while True:
+ time_slot = time_slots.get(index)
+ if time_slot is None:
+ break
+ slot_pulses[(time_slot[b"start_time"], time_slot[b"end_time"])] = [0]*size
+ index += 1
+ if len(slot_pulses) == 0:
+ slot_pulses[(0,1439*60)] = [0]*size
+ return slot_pulses
+
+
+
+def get_config()
+
+
+
Return the singleton configuration
+
+
+Expand source code
+
+
@staticmethod
+def get_config():
+ """ Return the singleton configuration """
+ if TimeSlotsConfig.config is None:
+ TimeSlotsConfig.config = TimeSlotsConfig()
+ TimeSlotsConfig.config.load()
+ return TimeSlotsConfig.config
+
+
+
+def get_cost(day)
+
+
+
Get the cost according to the day selected
+
+
+Expand source code
+
+
@staticmethod
+def get_cost(day):
+ """ Get the cost according to the day selected """
+ time_slots = TimeSlotsConfig.get_config()
+ rates = RatesConfig.get_config()
+ return time_slots.get_prices(rates.search_rates(day))
+
+
+
+
Methods
+
+
+def append(self, time_slot)
+
+
+
Add new time slot in the list
+
+
+Expand source code
+
+
def append(self, time_slot):
+ """ Add new time slot in the list """
+ found = False
+ time_slot = strings.tobytes(time_slot)
+ for current in self.time_slots:
+ if current[b"start_time"] == time_slot.start_time and current[b"end_time"] == time_slot.end_time:
+ current[b"color"] = time_slot.color
+ current[b"rate"] = time_slot.rate
+ found = True
+ break
+ if found is False:
+ self.time_slots.append(strings.tobytes(time_slot.__dict__))
+
+
+
+def get(self, index)
+
+
+
Return the time slot at the index
+
+
+Expand source code
+
+
def get(self, index):
+ """ Return the time slot at the index """
+ try:
+ return self.time_slots[int(index)]
+ except:
+ return None
+
+
+
+def get_prices(self, rates)
+
+
+
Return the list of prices according to the day
+
+
+Expand source code
+
+
def get_prices(self, rates):
+ """ Return the list of prices according to the day """
+ if rates == {}:
+ result = [{b'rate': b'', b'start_time': 0, b'end_time': 86340, b'color': b'#5498e0', b'price': 0, b'currency': b'not initialized'}]
+ else:
+ result = self.time_slots[:]
+ for time_slot in result:
+ time_slot[b"price"] = rates[time_slot[b"rate"]][b"price"]
+ time_slot[b"currency"] = rates[time_slot[b"rate"]][b"currency"]
+ return result
+
+
+
+def remove(self, index)
+
+
+
Remove the time slot at the index
+
+
+Expand source code
+
+
def remove(self, index):
+ """ Remove the time slot at the index """
+ try:
+ del self.time_slots[int(index)]
+ except:
+ pass
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/electricmeter/electricmeter.html b/doc/lib/electricmeter/electricmeter.html
new file mode 100644
index 0000000..191f17a
--- /dev/null
+++ b/doc/lib/electricmeter/electricmeter.html
@@ -0,0 +1,2127 @@
+
+
+
+
+
+
+lib.electricmeter.electricmeter API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module lib.electricmeter.electricmeter
+
+
+
Task to count wh from the electric meter
+
+
+Expand source code
+
+
""" Task to count wh from the electric meter """
+import struct
+import time
+import collections
+import uasyncio
+import machine
+import wifi
+from server.server import Server
+from electricmeter.config import TimeSlotsConfig
+from electricmeter import em_lang
+from tools import filesystem, date, strings, fnmatch, logger, tasking, info
+# pylint:disable=consider-using-f-string
+# pylint:disable=consider-iterating-dictionary
+# pylint:disable=missing-function-docstring
+# pylint:disable=global-variable-not-assigned
+# pylint:disable=trailing-whitespace
+# pylint:disable=too-many-lines
+
+# Hourly file format :
+# - pulses_per_minute:uint8_t * 24*60
+
+# Daily file format :
+# - (start_time:uint16_t, end_time:uint16_t, pulses_per_day:uint16_t * 31 days) * max_time_slots
+
+# Monthly file format :
+# - (start_time:uint16_t, end_time:uint16_t, pulses_per_month:uint32_t * 12 months) * max_time_slots
+
+PULSE_DIRECTORY = "pulses"
+PULSE_HOURLY = ".hourly"
+PULSE_DAILY = ".daily"
+PULSE_MONTHLY = ".monthly"
+
+class PulseSensor:
+ """ Detect wh pulse from electric meter """
+ def __init__(self, gpio, min_duration_ns=20_000_000, queue_length=1_500):
+ self.pulses = collections.deque((), queue_length)
+ self.notifier = uasyncio.Event()
+ self.previous_counter = 0
+ self.previous_time = time.time_ns()
+ self.min_duration_ns = min_duration_ns
+
+ # The use of pcnt counter allows to obtain a better reliability of counting
+ self.counter = machine.Counter(0, src=machine.Pin(gpio, mode=machine.Pin.IN), direction=machine.Counter.UP)
+ self.counter.filter_ns(self.min_duration_ns)
+ self.counter.pause()
+ self.counter.value(0)
+
+ self.sensor = machine.Pin(gpio, machine.Pin.IN, machine.Pin.PULL_DOWN)
+ self.sensor.irq(handler=self.detected, trigger=machine.Pin.IRQ_RISING)
+ self.counter.resume()
+
+ def __del__(self):
+ """ Destructor """
+ self.sensor.irq(handler=None)
+ self.counter.deinit()
+
+ def detected(self, pin):
+ """ Callback called when pulse detected """
+ pulse_time = time.time_ns()
+ pulse_counter = self.counter.value()
+ if pin.value() == 1:
+ # If new pulses detected
+ if pulse_counter != self.previous_counter:
+ # if the pulse is not too close to the previous one
+ if self.previous_time + self.min_duration_ns < pulse_time:
+ # If the new value is not a counter overflow
+ if pulse_counter > self.previous_counter:
+ pulse_quantity = pulse_counter - self.previous_counter
+ else:
+ pulse_quantity = 1
+
+ # Save the new counter value
+ self.previous_counter = pulse_counter
+
+ # Add the current pulse to list
+ self.pulses.append((pulse_quantity, pulse_time))
+
+ # Wake up pulse counter
+ self.notifier.set()
+ else:
+ # Ignore previous pulse
+ self.previous_counter = pulse_counter
+
+ async def wait(self):
+ """ Wait the wh pulses and the returns the list of pulses """
+ # Wait pulses
+ await self.notifier.wait()
+
+ # Clear notification flag
+ self.notifier.clear()
+ result = []
+
+ # Empty list of pulses
+ while True:
+ try:
+ result.append(self.pulses.popleft())
+ except IndexError:
+ break
+ return result
+
+ def simulate(self, pulses):
+ """ Simulates the flashing led on the electric meter """
+ self.sensor.value(1)
+ self.counter.value(pulses)
+ self.detected(self.sensor)
+
+class HourlyCounter:
+ """ Hourly counter of wh consumed """
+ counter = None
+
+ def __init__(self, gpio):
+ """ Constructor """
+ global PULSE_DIRECTORY
+ filesystem.makedir(PULSE_DIRECTORY,True)
+ self.day = time.time()
+ self.pulses = HourlyCounter.load(HourlyCounter.get_filename(self.day))
+ self.last_save = time.time()
+ self.watt_hour = 0
+ self.previous_pulse_time = None
+ self.day_pulses_counter = 0
+ self.pulse_sensor = PulseSensor(gpio)
+
+ async def manage(self):
+ """ Manage the counter """
+ # Wait the list of pulses
+ pulses = await self.pulse_sensor.wait()
+ for pulse_quantity, pulse_time in pulses:
+ # If previous pulse registered
+ if self.previous_pulse_time is not None:
+ # Compute the instant power
+ self.watt_hour = 3600 * 1_000_000_000/((pulse_time - self.previous_pulse_time)/pulse_quantity)
+
+ # Increase
+ self.day_pulses_counter += pulse_quantity
+ self.previous_pulse_time = pulse_time
+ hour,minute=date.local_time(pulse_time//1_000_000_000)[3:5]
+ day = pulse_time//1_000_000_000
+ minut = hour*60+minute
+ self.pulses[minut] += pulse_quantity
+
+ # If day change
+ if HourlyCounter.is_same_day(day, self.day) is False:
+ # Save day counter
+ HourlyCounter.save(HourlyCounter.get_filename(self.day))
+
+ # Clear day pulses counter
+ self.day_pulses_counter = 0
+
+ # Load new day if existing or clear counter (manage day light saving)
+ self.pulses = self.load(day)
+ self.day = day
+
+ # Each ten minutes
+ if time.time() > (self.last_save + 67):
+ # Save pulses file
+ HourlyCounter.save(HourlyCounter.get_filename(self.day))
+
+ # print("%s %.1f wh pulses=%d"%(date.date_to_string(), self.watt_hour, self.day_pulses_counter))
+ return True
+
+ @staticmethod
+ def is_same_day(day1, day2):
+ """ Indicates if the day 1 is equal to day 2"""
+ if day1//86400 == day2//86400 or day1 is None or day2 is None:
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def get_filename(day=None):
+ """ Return the filename according to day """
+ global PULSE_DIRECTORY, PULSE_HOURLY
+ if day is None:
+ day = time.time()
+ year,month,day = date.local_time(day)[:3]
+ return "%s/%04d-%02d/%04d-%02d-%02d%s"%(PULSE_DIRECTORY, year, month, year, month, day, PULSE_HOURLY)
+
+ @staticmethod
+ def save(filename):
+ """ Save pulses file """
+ try:
+ filesystem.makedir(filesystem.split(filename)[0], True)
+ with open(filename, "wb") as file:
+ file.write(struct.pack("B"*1440, *HourlyCounter.counter.pulses))
+ HourlyCounter.counter.last_save = time.time()
+ except Exception as err:
+ logger.exception(err)
+
+ @staticmethod
+ def load(filename, pulses=None):
+ """ Load pulses file """
+ result = [0]*1440
+ try:
+ with open(filename, "rb") as file:
+ load_pulses = struct.unpack("B"*1440, file.read())
+ index = 0
+ if pulses is None:
+ result = list(load_pulses)
+ else:
+ result = pulses
+ for pulse in load_pulses:
+ result[index] += pulse
+ index += 1
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+ @staticmethod
+ def get_power():
+ """ Return power instantaneous """
+ return HourlyCounter.counter.watt_hour
+
+ def simulate(self, pulses):
+ """ Simulates the flashing led on the electric meter """
+ self.pulse_sensor.simulate(pulses)
+
+ @staticmethod
+ async def task(gpio):
+ """ Task to count wh from the electric meter """
+ HourlyCounter.counter = HourlyCounter(gpio)
+ await tasking.task_monitoring(HourlyCounter.counter.manage)
+
+ @staticmethod
+ def get_datas(selected_date):
+ """ Return the pulse filename according to the selected date """
+ if type(selected_date) == type(b""):
+ selected_date = date.html_to_date(selected_date)
+
+ if selected_date is None or HourlyCounter.is_same_day(selected_date, time.time()):
+ result = HourlyCounter.counter.pulses
+ else:
+ result = HourlyCounter.load(HourlyCounter.get_filename(selected_date))
+ return result
+
+ @staticmethod
+ async def pulse_simulator():
+ """ Simulates the flashing led on the electric meter """
+ import random
+ while True:
+ HourlyCounter.counter.simulate(random.randint(1, 4))
+ await uasyncio.sleep(random.randint(1, 5))
+
+class DailyCounter:
+ """ Daily counter of wh consumed """
+ @staticmethod
+ def get_filename(selected_date=None):
+ """ Return the filename according to selected date """
+ global PULSE_DIRECTORY, PULSE_DAILY
+ if selected_date is None:
+ selected_date = time.time()
+ year,month = date.local_time(selected_date)[:2]
+ return "%s/%04d-%02d/%04d-%02d%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_DAILY)
+
+ @staticmethod
+ def get_datas(selected_date):
+ """ Return the pulse filename according to the month selected """
+ result = []
+ filename = DailyCounter.get_filename(selected_date)
+ slot_pulses = DailyCounter.load(filename)
+ for time_slot, days in slot_pulses.items():
+ result.append({"time_slot":time_slot,"days":days})
+ return result
+
+ @staticmethod
+ def load(filename):
+ """ Load daily file """
+ result = {}
+ try:
+ with open(filename, "rb") as file:
+ while True:
+ data = file.read(2)
+ if len(data) == 0:
+ break
+ start = struct.unpack("H", data)[0]*60
+ end = struct.unpack("H", file.read(2))[0]*60
+ days = struct.unpack("H"*31, file.read(2*31))
+ result[(start,end)] = days
+ except OSError:
+ pass
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+ @staticmethod
+ def save(filename, slot_pulses):
+ """ Save daily file """
+ try:
+ with open(filename, "wb") as file:
+ for time_slot, days in slot_pulses.items():
+ start, end = time_slot
+ file.write(struct.pack("HH", start//60, end//60))
+ file.write(struct.pack("H"*len(days), *days))
+ except OSError:
+ pass
+ except Exception as err:
+ logger.exception(err)
+
+ @staticmethod
+ async def update(filenames, daily_to_update):
+ """ Update daily files """
+ for key, daily_filename in daily_to_update.items():
+ year, month = key
+ print("Update %s\n "%daily_filename, end="")
+ hourly_searched = "%s/%s-%s/%s-%s*%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_HOURLY)
+ slot_pulses = TimeSlotsConfig.create_empty_slot(31)
+ for hourly_filename in filenames:
+ if fnmatch.fnmatch(hourly_filename, hourly_searched):
+ name = filesystem.splitext(filesystem.split(hourly_filename)[1])[0]
+ day = int(name.split("-")[-1])
+ print("%d "%day, end="")
+ hourly_pulses = HourlyCounter.load(hourly_filename)
+ second = 0
+ for start, end in slot_pulses.keys():
+ second = 0
+ for pulses in hourly_pulses:
+ if start <= second <= end:
+ slot_pulses[(start,end)][day-1] += pulses
+ second += 60
+ else:
+ pass
+ await uasyncio.sleep_ms(2)
+ print("")
+ await uasyncio.sleep_ms(2)
+ DailyCounter.save(daily_filename, slot_pulses)
+
+class MonthlyCounter:
+ """ Monthly counter of wh consumed """
+ last_update = [0]
+ next_update = [0]
+ force = [True]
+
+ @staticmethod
+ async def update(filenames, monthly_to_update):
+ """ Update monthly files """
+ for year, monthly_filename in monthly_to_update.items():
+ print("Update %s\n "%monthly_filename, end="")
+ daily_searched = "%s/%s-*/%s-*%s"%(PULSE_DIRECTORY, year, year, PULSE_DAILY)
+ slot_pulses = TimeSlotsConfig.create_empty_slot(12)
+ for daily_filename in filenames:
+ if fnmatch.fnmatch(daily_filename, daily_searched):
+ name = filesystem.splitext(filesystem.split(daily_filename)[1])[0]
+ month = int(name.split("-")[-1])
+ print("%d "%month, end="")
+ daily_slot_pulses = DailyCounter.load(daily_filename)
+ for time_slot, days in daily_slot_pulses.items():
+ for day in days:
+ slot_pulses[time_slot][month-1] = slot_pulses[time_slot][month-1]+day
+ await uasyncio.sleep_ms(2)
+ print("")
+ MonthlyCounter.save(monthly_filename, slot_pulses)
+ await uasyncio.sleep_ms(2)
+
+ @staticmethod
+ def save(filename, slot_pulses):
+ try:
+ with open(filename, "wb") as file:
+ for time_slot, months in slot_pulses.items():
+ start, end = time_slot
+ file.write(struct.pack("HH", start//60, end//60))
+ file.write(struct.pack("I"*len(months), *months))
+ except Exception as err:
+ logger.exception(err)
+
+ @staticmethod
+ def load(filename):
+ """ Load daily file content """
+ result = {}
+ try:
+ with open(filename, "rb") as file:
+ while True:
+ data = file.read(2)
+ if len(data) == 0:
+ break
+ start = struct.unpack("H", data)[0]*60
+ end = struct.unpack("H", file.read(2))[0]*60
+ months = struct.unpack("I"*12, file.read(4*12))
+ result[(start,end)] = months
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+ @staticmethod
+ async def get_updates():
+ """ Build the list of daily and monthly file to update """
+ global PULSE_DIRECTORY, PULSE_MONTHLY, PULSE_DAILY
+ force = MonthlyCounter.force[0]
+ _, filenames = await filesystem.ascandir(PULSE_DIRECTORY, "*", True)
+ filenames.sort()
+ daily_to_update = {}
+ monthly_to_update = {}
+ for filename in filenames:
+ if fnmatch.fnmatch(filename, "*"+PULSE_HOURLY):
+ name = filesystem.splitext(filesystem.split(filename)[1])[0]
+ year, month, day = name.split("-")
+ daily = "%s/%s-%s/%s-%s%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_DAILY)
+ monthly = "%s/%s%s"%(PULSE_DIRECTORY, year, PULSE_MONTHLY)
+ update = False
+ if daily not in daily_to_update:
+ if filesystem.exists(daily):
+ daily_date = filesystem.fileinfo(daily)[8]
+ hourly_date = filesystem.fileinfo(filename)[8]
+ if hourly_date > daily_date or force:
+ update = True
+ else:
+ update = True
+ if update:
+ daily_to_update[(year, month)] = daily
+ monthly_to_update[year] = monthly
+ await uasyncio.sleep_ms(2)
+ MonthlyCounter.force[0] = False
+ return daily_to_update, monthly_to_update, filenames
+
+ @staticmethod
+ def get_filename(selected_date=None):
+ """ Return the filename according to day """
+ global PULSE_DIRECTORY, PULSE_MONTHLY
+ if selected_date is None:
+ selected_date = time.time()
+ year = date.local_time(selected_date)[0]
+ return "%s/%04d%s"%(PULSE_DIRECTORY, year, PULSE_MONTHLY)
+
+ @staticmethod
+ def get_datas(selected_date):
+ """ Return the pulse filename according to the month selected """
+ result = []
+ slot_pulses = MonthlyCounter.load(MonthlyCounter.get_filename(selected_date))
+ for time_slot, months in slot_pulses.items():
+ result.append({"time_slot":time_slot,"months":months})
+ return result
+
+ @staticmethod
+ async def refresh():
+ """ Refresh the counters """
+ if MonthlyCounter.last_update[0] + 599 < time.time():
+ MonthlyCounter.next_update[0] = 0
+
+ @staticmethod
+ async def manage():
+ """ Rebuild all month files if necessary """
+ while True:
+ if MonthlyCounter.next_update[0] <= 0:
+ break
+ MonthlyCounter.next_update[0] -= 1
+ await uasyncio.sleep(1)
+
+ daily_to_update, monthly_to_update, filenames = await MonthlyCounter().get_updates()
+ await DailyCounter.update (filenames, daily_to_update)
+ await MonthlyCounter.update(filenames, monthly_to_update)
+ MonthlyCounter.next_update[0] = 28793
+ MonthlyCounter.last_update[0] = time.time()
+ return True
+
+ @staticmethod
+ async def task():
+ """ Task to count wh from the electric meter """
+ await tasking.task_monitoring(MonthlyCounter.manage)
+
+class Consumption:
+ """ Stores the consumption of a time slot """
+ def __init__(self, name, currency):
+ """ Constructor """
+ self.name = strings.tostrings(name)
+ self.cost = 0.
+ self.pulses = 0
+ self.currency = strings.tostrings(currency)
+
+ def add(self, pulses, price):
+ """ Add wh pulses with its price """
+ cumul_pulses = 0
+ for pulse in pulses:
+ cumul_pulses += pulse
+
+ self.cost += cumul_pulses*price/1000
+ self.pulses += cumul_pulses
+
+ def to_string(self):
+ """ Convert to string """
+ return "%s : %.2f %s (%.3f kWh)"%(self.name, self.cost, strings.tostrings(self.currency), self.pulses/1000)
+
+class Cost:
+ """ Abstract class to compute the cost """
+ def get_rates(self, selected_date):
+ """ Get the rate according to the selected date """
+ prices = TimeSlotsConfig.get_cost(selected_date)
+ consumptions = {}
+ for price in prices:
+ consumptions[price[b"rate"]] = Consumption(price[b"rate"], price[b"currency"])
+ return prices, consumptions
+
+ def compute(self, selected_date):
+ """ Compute cost """
+ return {}
+
+ def get_message(self, title, selected_date):
+ """ Get result message """
+ consumptions = self.compute(selected_date)
+ result = " - " + strings.tostrings(title) + " :\n"
+ for consumption in consumptions.values():
+ result += " %s\n"%consumption.to_string()
+ return result
+
+class HourlyCost(Cost):
+ """ Hourly cost calculation """
+ def compute(self, selected_date):
+ """ Compute cost """
+ prices, consumptions = self.get_rates(selected_date)
+ pulses = HourlyCounter.load(HourlyCounter.get_filename(selected_date))
+ second = 0
+ for pulse in pulses:
+ for price in prices:
+ if price[b"start_time"] <= second <= price[b"end_time"]:
+ consumptions[price[b"rate"]].add([pulse], price[b"price"])
+ break
+ second += 60
+ return consumptions
+
+class DailyCost(Cost):
+ """ Daily cost calculation """
+ def compute(self, selected_date):
+ """ Compute cost """
+ slot_pulses = DailyCounter.load(DailyCounter.get_filename(selected_date))
+ prices,consumptions = self.get_rates(selected_date)
+ for slot_time, pulses in slot_pulses.items():
+ for price in prices:
+ start, end = slot_time
+ if price[b"start_time"] == start and price[b"end_time"] == end:
+ consumptions[price[b"rate"]].add(pulses, price[b"price"])
+ break
+ return consumptions
+
+def daily_notifier():
+ """ Get electricmeter daily notification """
+ selected_date = time.time() - 86400
+ message = "\n"
+ cost = HourlyCost()
+ message += cost.get_message(em_lang.item_day, selected_date)
+ cost = DailyCost()
+ message += cost.get_message(em_lang.item_month, selected_date)
+ message += " - Lan Ip : %s\n"%wifi.Station.get_info()[0]
+ message += " - Wan Ip : %s\n"%Server.context.wan_ip
+ message += " - Uptime : %s\n"%strings.tostrings(info.uptime())
+ return message
+
+def create_electric_meter(loop, gpio=21):
+ """ Create user task """
+ Server.set_daily_notifier(daily_notifier)
+ loop.create_task(HourlyCounter.task(gpio))
+ loop.create_task(MonthlyCounter.task())
+ if filesystem.ismicropython() is False:
+ loop.create_task(HourlyCounter.pulse_simulator())
+
+
+
+
+
+
+
+
Functions
+
+
+def create_electric_meter(loop, gpio=21)
+
+
+
Create user task
+
+
+Expand source code
+
+
def create_electric_meter(loop, gpio=21):
+ """ Create user task """
+ Server.set_daily_notifier(daily_notifier)
+ loop.create_task(HourlyCounter.task(gpio))
+ loop.create_task(MonthlyCounter.task())
+ if filesystem.ismicropython() is False:
+ loop.create_task(HourlyCounter.pulse_simulator())
def get_message(self, title, selected_date):
+ """ Get result message """
+ consumptions = self.compute(selected_date)
+ result = " - " + strings.tostrings(title) + " :\n"
+ for consumption in consumptions.values():
+ result += " %s\n"%consumption.to_string()
+ return result
+
+
+
+def get_rates(self, selected_date)
+
+
+
Get the rate according to the selected date
+
+
+Expand source code
+
+
def get_rates(self, selected_date):
+ """ Get the rate according to the selected date """
+ prices = TimeSlotsConfig.get_cost(selected_date)
+ consumptions = {}
+ for price in prices:
+ consumptions[price[b"rate"]] = Consumption(price[b"rate"], price[b"currency"])
+ return prices, consumptions
+
+
+
+
+
+class DailyCost
+
+
+
Daily cost calculation
+
+
+Expand source code
+
+
class DailyCost(Cost):
+ """ Daily cost calculation """
+ def compute(self, selected_date):
+ """ Compute cost """
+ slot_pulses = DailyCounter.load(DailyCounter.get_filename(selected_date))
+ prices,consumptions = self.get_rates(selected_date)
+ for slot_time, pulses in slot_pulses.items():
+ for price in prices:
+ start, end = slot_time
+ if price[b"start_time"] == start and price[b"end_time"] == end:
+ consumptions[price[b"rate"]].add(pulses, price[b"price"])
+ break
+ return consumptions
class DailyCounter:
+ """ Daily counter of wh consumed """
+ @staticmethod
+ def get_filename(selected_date=None):
+ """ Return the filename according to selected date """
+ global PULSE_DIRECTORY, PULSE_DAILY
+ if selected_date is None:
+ selected_date = time.time()
+ year,month = date.local_time(selected_date)[:2]
+ return "%s/%04d-%02d/%04d-%02d%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_DAILY)
+
+ @staticmethod
+ def get_datas(selected_date):
+ """ Return the pulse filename according to the month selected """
+ result = []
+ filename = DailyCounter.get_filename(selected_date)
+ slot_pulses = DailyCounter.load(filename)
+ for time_slot, days in slot_pulses.items():
+ result.append({"time_slot":time_slot,"days":days})
+ return result
+
+ @staticmethod
+ def load(filename):
+ """ Load daily file """
+ result = {}
+ try:
+ with open(filename, "rb") as file:
+ while True:
+ data = file.read(2)
+ if len(data) == 0:
+ break
+ start = struct.unpack("H", data)[0]*60
+ end = struct.unpack("H", file.read(2))[0]*60
+ days = struct.unpack("H"*31, file.read(2*31))
+ result[(start,end)] = days
+ except OSError:
+ pass
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+ @staticmethod
+ def save(filename, slot_pulses):
+ """ Save daily file """
+ try:
+ with open(filename, "wb") as file:
+ for time_slot, days in slot_pulses.items():
+ start, end = time_slot
+ file.write(struct.pack("HH", start//60, end//60))
+ file.write(struct.pack("H"*len(days), *days))
+ except OSError:
+ pass
+ except Exception as err:
+ logger.exception(err)
+
+ @staticmethod
+ async def update(filenames, daily_to_update):
+ """ Update daily files """
+ for key, daily_filename in daily_to_update.items():
+ year, month = key
+ print("Update %s\n "%daily_filename, end="")
+ hourly_searched = "%s/%s-%s/%s-%s*%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_HOURLY)
+ slot_pulses = TimeSlotsConfig.create_empty_slot(31)
+ for hourly_filename in filenames:
+ if fnmatch.fnmatch(hourly_filename, hourly_searched):
+ name = filesystem.splitext(filesystem.split(hourly_filename)[1])[0]
+ day = int(name.split("-")[-1])
+ print("%d "%day, end="")
+ hourly_pulses = HourlyCounter.load(hourly_filename)
+ second = 0
+ for start, end in slot_pulses.keys():
+ second = 0
+ for pulses in hourly_pulses:
+ if start <= second <= end:
+ slot_pulses[(start,end)][day-1] += pulses
+ second += 60
+ else:
+ pass
+ await uasyncio.sleep_ms(2)
+ print("")
+ await uasyncio.sleep_ms(2)
+ DailyCounter.save(daily_filename, slot_pulses)
+
+
Static methods
+
+
+def get_datas(selected_date)
+
+
+
Return the pulse filename according to the month selected
+
+
+Expand source code
+
+
@staticmethod
+def get_datas(selected_date):
+ """ Return the pulse filename according to the month selected """
+ result = []
+ filename = DailyCounter.get_filename(selected_date)
+ slot_pulses = DailyCounter.load(filename)
+ for time_slot, days in slot_pulses.items():
+ result.append({"time_slot":time_slot,"days":days})
+ return result
+
+
+
+def get_filename(selected_date=None)
+
+
+
Return the filename according to selected date
+
+
+Expand source code
+
+
@staticmethod
+def get_filename(selected_date=None):
+ """ Return the filename according to selected date """
+ global PULSE_DIRECTORY, PULSE_DAILY
+ if selected_date is None:
+ selected_date = time.time()
+ year,month = date.local_time(selected_date)[:2]
+ return "%s/%04d-%02d/%04d-%02d%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_DAILY)
+
+
+
+def load(filename)
+
+
+
Load daily file
+
+
+Expand source code
+
+
@staticmethod
+def load(filename):
+ """ Load daily file """
+ result = {}
+ try:
+ with open(filename, "rb") as file:
+ while True:
+ data = file.read(2)
+ if len(data) == 0:
+ break
+ start = struct.unpack("H", data)[0]*60
+ end = struct.unpack("H", file.read(2))[0]*60
+ days = struct.unpack("H"*31, file.read(2*31))
+ result[(start,end)] = days
+ except OSError:
+ pass
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+
+
+def save(filename, slot_pulses)
+
+
+
Save daily file
+
+
+Expand source code
+
+
@staticmethod
+def save(filename, slot_pulses):
+ """ Save daily file """
+ try:
+ with open(filename, "wb") as file:
+ for time_slot, days in slot_pulses.items():
+ start, end = time_slot
+ file.write(struct.pack("HH", start//60, end//60))
+ file.write(struct.pack("H"*len(days), *days))
+ except OSError:
+ pass
+ except Exception as err:
+ logger.exception(err)
+
+
+
+async def update(filenames, daily_to_update)
+
+
+
Update daily files
+
+
+Expand source code
+
+
@staticmethod
+async def update(filenames, daily_to_update):
+ """ Update daily files """
+ for key, daily_filename in daily_to_update.items():
+ year, month = key
+ print("Update %s\n "%daily_filename, end="")
+ hourly_searched = "%s/%s-%s/%s-%s*%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_HOURLY)
+ slot_pulses = TimeSlotsConfig.create_empty_slot(31)
+ for hourly_filename in filenames:
+ if fnmatch.fnmatch(hourly_filename, hourly_searched):
+ name = filesystem.splitext(filesystem.split(hourly_filename)[1])[0]
+ day = int(name.split("-")[-1])
+ print("%d "%day, end="")
+ hourly_pulses = HourlyCounter.load(hourly_filename)
+ second = 0
+ for start, end in slot_pulses.keys():
+ second = 0
+ for pulses in hourly_pulses:
+ if start <= second <= end:
+ slot_pulses[(start,end)][day-1] += pulses
+ second += 60
+ else:
+ pass
+ await uasyncio.sleep_ms(2)
+ print("")
+ await uasyncio.sleep_ms(2)
+ DailyCounter.save(daily_filename, slot_pulses)
+
+
+
+
+
+class HourlyCost
+
+
+
Hourly cost calculation
+
+
+Expand source code
+
+
class HourlyCost(Cost):
+ """ Hourly cost calculation """
+ def compute(self, selected_date):
+ """ Compute cost """
+ prices, consumptions = self.get_rates(selected_date)
+ pulses = HourlyCounter.load(HourlyCounter.get_filename(selected_date))
+ second = 0
+ for pulse in pulses:
+ for price in prices:
+ if price[b"start_time"] <= second <= price[b"end_time"]:
+ consumptions[price[b"rate"]].add([pulse], price[b"price"])
+ break
+ second += 60
+ return consumptions
class HourlyCounter:
+ """ Hourly counter of wh consumed """
+ counter = None
+
+ def __init__(self, gpio):
+ """ Constructor """
+ global PULSE_DIRECTORY
+ filesystem.makedir(PULSE_DIRECTORY,True)
+ self.day = time.time()
+ self.pulses = HourlyCounter.load(HourlyCounter.get_filename(self.day))
+ self.last_save = time.time()
+ self.watt_hour = 0
+ self.previous_pulse_time = None
+ self.day_pulses_counter = 0
+ self.pulse_sensor = PulseSensor(gpio)
+
+ async def manage(self):
+ """ Manage the counter """
+ # Wait the list of pulses
+ pulses = await self.pulse_sensor.wait()
+ for pulse_quantity, pulse_time in pulses:
+ # If previous pulse registered
+ if self.previous_pulse_time is not None:
+ # Compute the instant power
+ self.watt_hour = 3600 * 1_000_000_000/((pulse_time - self.previous_pulse_time)/pulse_quantity)
+
+ # Increase
+ self.day_pulses_counter += pulse_quantity
+ self.previous_pulse_time = pulse_time
+ hour,minute=date.local_time(pulse_time//1_000_000_000)[3:5]
+ day = pulse_time//1_000_000_000
+ minut = hour*60+minute
+ self.pulses[minut] += pulse_quantity
+
+ # If day change
+ if HourlyCounter.is_same_day(day, self.day) is False:
+ # Save day counter
+ HourlyCounter.save(HourlyCounter.get_filename(self.day))
+
+ # Clear day pulses counter
+ self.day_pulses_counter = 0
+
+ # Load new day if existing or clear counter (manage day light saving)
+ self.pulses = self.load(day)
+ self.day = day
+
+ # Each ten minutes
+ if time.time() > (self.last_save + 67):
+ # Save pulses file
+ HourlyCounter.save(HourlyCounter.get_filename(self.day))
+
+ # print("%s %.1f wh pulses=%d"%(date.date_to_string(), self.watt_hour, self.day_pulses_counter))
+ return True
+
+ @staticmethod
+ def is_same_day(day1, day2):
+ """ Indicates if the day 1 is equal to day 2"""
+ if day1//86400 == day2//86400 or day1 is None or day2 is None:
+ return True
+ else:
+ return False
+
+ @staticmethod
+ def get_filename(day=None):
+ """ Return the filename according to day """
+ global PULSE_DIRECTORY, PULSE_HOURLY
+ if day is None:
+ day = time.time()
+ year,month,day = date.local_time(day)[:3]
+ return "%s/%04d-%02d/%04d-%02d-%02d%s"%(PULSE_DIRECTORY, year, month, year, month, day, PULSE_HOURLY)
+
+ @staticmethod
+ def save(filename):
+ """ Save pulses file """
+ try:
+ filesystem.makedir(filesystem.split(filename)[0], True)
+ with open(filename, "wb") as file:
+ file.write(struct.pack("B"*1440, *HourlyCounter.counter.pulses))
+ HourlyCounter.counter.last_save = time.time()
+ except Exception as err:
+ logger.exception(err)
+
+ @staticmethod
+ def load(filename, pulses=None):
+ """ Load pulses file """
+ result = [0]*1440
+ try:
+ with open(filename, "rb") as file:
+ load_pulses = struct.unpack("B"*1440, file.read())
+ index = 0
+ if pulses is None:
+ result = list(load_pulses)
+ else:
+ result = pulses
+ for pulse in load_pulses:
+ result[index] += pulse
+ index += 1
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+ @staticmethod
+ def get_power():
+ """ Return power instantaneous """
+ return HourlyCounter.counter.watt_hour
+
+ def simulate(self, pulses):
+ """ Simulates the flashing led on the electric meter """
+ self.pulse_sensor.simulate(pulses)
+
+ @staticmethod
+ async def task(gpio):
+ """ Task to count wh from the electric meter """
+ HourlyCounter.counter = HourlyCounter(gpio)
+ await tasking.task_monitoring(HourlyCounter.counter.manage)
+
+ @staticmethod
+ def get_datas(selected_date):
+ """ Return the pulse filename according to the selected date """
+ if type(selected_date) == type(b""):
+ selected_date = date.html_to_date(selected_date)
+
+ if selected_date is None or HourlyCounter.is_same_day(selected_date, time.time()):
+ result = HourlyCounter.counter.pulses
+ else:
+ result = HourlyCounter.load(HourlyCounter.get_filename(selected_date))
+ return result
+
+ @staticmethod
+ async def pulse_simulator():
+ """ Simulates the flashing led on the electric meter """
+ import random
+ while True:
+ HourlyCounter.counter.simulate(random.randint(1, 4))
+ await uasyncio.sleep(random.randint(1, 5))
+
+
Class variables
+
+
var counter
+
+
+
+
+
Static methods
+
+
+def get_datas(selected_date)
+
+
+
Return the pulse filename according to the selected date
+
+
+Expand source code
+
+
@staticmethod
+def get_datas(selected_date):
+ """ Return the pulse filename according to the selected date """
+ if type(selected_date) == type(b""):
+ selected_date = date.html_to_date(selected_date)
+
+ if selected_date is None or HourlyCounter.is_same_day(selected_date, time.time()):
+ result = HourlyCounter.counter.pulses
+ else:
+ result = HourlyCounter.load(HourlyCounter.get_filename(selected_date))
+ return result
+
+
+
+def get_filename(day=None)
+
+
+
Return the filename according to day
+
+
+Expand source code
+
+
@staticmethod
+def get_filename(day=None):
+ """ Return the filename according to day """
+ global PULSE_DIRECTORY, PULSE_HOURLY
+ if day is None:
+ day = time.time()
+ year,month,day = date.local_time(day)[:3]
+ return "%s/%04d-%02d/%04d-%02d-%02d%s"%(PULSE_DIRECTORY, year, month, year, month, day, PULSE_HOURLY)
@staticmethod
+def is_same_day(day1, day2):
+ """ Indicates if the day 1 is equal to day 2"""
+ if day1//86400 == day2//86400 or day1 is None or day2 is None:
+ return True
+ else:
+ return False
+
+
+
+def load(filename, pulses=None)
+
+
+
Load pulses file
+
+
+Expand source code
+
+
@staticmethod
+def load(filename, pulses=None):
+ """ Load pulses file """
+ result = [0]*1440
+ try:
+ with open(filename, "rb") as file:
+ load_pulses = struct.unpack("B"*1440, file.read())
+ index = 0
+ if pulses is None:
+ result = list(load_pulses)
+ else:
+ result = pulses
+ for pulse in load_pulses:
+ result[index] += pulse
+ index += 1
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+
+
+async def pulse_simulator()
+
+
+
Simulates the flashing led on the electric meter
+
+
+Expand source code
+
+
@staticmethod
+async def pulse_simulator():
+ """ Simulates the flashing led on the electric meter """
+ import random
+ while True:
+ HourlyCounter.counter.simulate(random.randint(1, 4))
+ await uasyncio.sleep(random.randint(1, 5))
+
+
+
+def save(filename)
+
+
+
Save pulses file
+
+
+Expand source code
+
+
@staticmethod
+def save(filename):
+ """ Save pulses file """
+ try:
+ filesystem.makedir(filesystem.split(filename)[0], True)
+ with open(filename, "wb") as file:
+ file.write(struct.pack("B"*1440, *HourlyCounter.counter.pulses))
+ HourlyCounter.counter.last_save = time.time()
+ except Exception as err:
+ logger.exception(err)
+
+
+
+async def task(gpio)
+
+
+
Task to count wh from the electric meter
+
+
+Expand source code
+
+
@staticmethod
+async def task(gpio):
+ """ Task to count wh from the electric meter """
+ HourlyCounter.counter = HourlyCounter(gpio)
+ await tasking.task_monitoring(HourlyCounter.counter.manage)
+
+
+
+
Methods
+
+
+async def manage(self)
+
+
+
Manage the counter
+
+
+Expand source code
+
+
async def manage(self):
+ """ Manage the counter """
+ # Wait the list of pulses
+ pulses = await self.pulse_sensor.wait()
+ for pulse_quantity, pulse_time in pulses:
+ # If previous pulse registered
+ if self.previous_pulse_time is not None:
+ # Compute the instant power
+ self.watt_hour = 3600 * 1_000_000_000/((pulse_time - self.previous_pulse_time)/pulse_quantity)
+
+ # Increase
+ self.day_pulses_counter += pulse_quantity
+ self.previous_pulse_time = pulse_time
+ hour,minute=date.local_time(pulse_time//1_000_000_000)[3:5]
+ day = pulse_time//1_000_000_000
+ minut = hour*60+minute
+ self.pulses[minut] += pulse_quantity
+
+ # If day change
+ if HourlyCounter.is_same_day(day, self.day) is False:
+ # Save day counter
+ HourlyCounter.save(HourlyCounter.get_filename(self.day))
+
+ # Clear day pulses counter
+ self.day_pulses_counter = 0
+
+ # Load new day if existing or clear counter (manage day light saving)
+ self.pulses = self.load(day)
+ self.day = day
+
+ # Each ten minutes
+ if time.time() > (self.last_save + 67):
+ # Save pulses file
+ HourlyCounter.save(HourlyCounter.get_filename(self.day))
+
+ # print("%s %.1f wh pulses=%d"%(date.date_to_string(), self.watt_hour, self.day_pulses_counter))
+ return True
+
+
+
+def simulate(self, pulses)
+
+
+
Simulates the flashing led on the electric meter
+
+
+Expand source code
+
+
def simulate(self, pulses):
+ """ Simulates the flashing led on the electric meter """
+ self.pulse_sensor.simulate(pulses)
+
+
+
+
+
+class MonthlyCounter
+
+
+
Monthly counter of wh consumed
+
+
+Expand source code
+
+
class MonthlyCounter:
+ """ Monthly counter of wh consumed """
+ last_update = [0]
+ next_update = [0]
+ force = [True]
+
+ @staticmethod
+ async def update(filenames, monthly_to_update):
+ """ Update monthly files """
+ for year, monthly_filename in monthly_to_update.items():
+ print("Update %s\n "%monthly_filename, end="")
+ daily_searched = "%s/%s-*/%s-*%s"%(PULSE_DIRECTORY, year, year, PULSE_DAILY)
+ slot_pulses = TimeSlotsConfig.create_empty_slot(12)
+ for daily_filename in filenames:
+ if fnmatch.fnmatch(daily_filename, daily_searched):
+ name = filesystem.splitext(filesystem.split(daily_filename)[1])[0]
+ month = int(name.split("-")[-1])
+ print("%d "%month, end="")
+ daily_slot_pulses = DailyCounter.load(daily_filename)
+ for time_slot, days in daily_slot_pulses.items():
+ for day in days:
+ slot_pulses[time_slot][month-1] = slot_pulses[time_slot][month-1]+day
+ await uasyncio.sleep_ms(2)
+ print("")
+ MonthlyCounter.save(monthly_filename, slot_pulses)
+ await uasyncio.sleep_ms(2)
+
+ @staticmethod
+ def save(filename, slot_pulses):
+ try:
+ with open(filename, "wb") as file:
+ for time_slot, months in slot_pulses.items():
+ start, end = time_slot
+ file.write(struct.pack("HH", start//60, end//60))
+ file.write(struct.pack("I"*len(months), *months))
+ except Exception as err:
+ logger.exception(err)
+
+ @staticmethod
+ def load(filename):
+ """ Load daily file content """
+ result = {}
+ try:
+ with open(filename, "rb") as file:
+ while True:
+ data = file.read(2)
+ if len(data) == 0:
+ break
+ start = struct.unpack("H", data)[0]*60
+ end = struct.unpack("H", file.read(2))[0]*60
+ months = struct.unpack("I"*12, file.read(4*12))
+ result[(start,end)] = months
+ except Exception as err:
+ logger.exception(err)
+ return result
+
+ @staticmethod
+ async def get_updates():
+ """ Build the list of daily and monthly file to update """
+ global PULSE_DIRECTORY, PULSE_MONTHLY, PULSE_DAILY
+ force = MonthlyCounter.force[0]
+ _, filenames = await filesystem.ascandir(PULSE_DIRECTORY, "*", True)
+ filenames.sort()
+ daily_to_update = {}
+ monthly_to_update = {}
+ for filename in filenames:
+ if fnmatch.fnmatch(filename, "*"+PULSE_HOURLY):
+ name = filesystem.splitext(filesystem.split(filename)[1])[0]
+ year, month, day = name.split("-")
+ daily = "%s/%s-%s/%s-%s%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_DAILY)
+ monthly = "%s/%s%s"%(PULSE_DIRECTORY, year, PULSE_MONTHLY)
+ update = False
+ if daily not in daily_to_update:
+ if filesystem.exists(daily):
+ daily_date = filesystem.fileinfo(daily)[8]
+ hourly_date = filesystem.fileinfo(filename)[8]
+ if hourly_date > daily_date or force:
+ update = True
+ else:
+ update = True
+ if update:
+ daily_to_update[(year, month)] = daily
+ monthly_to_update[year] = monthly
+ await uasyncio.sleep_ms(2)
+ MonthlyCounter.force[0] = False
+ return daily_to_update, monthly_to_update, filenames
+
+ @staticmethod
+ def get_filename(selected_date=None):
+ """ Return the filename according to day """
+ global PULSE_DIRECTORY, PULSE_MONTHLY
+ if selected_date is None:
+ selected_date = time.time()
+ year = date.local_time(selected_date)[0]
+ return "%s/%04d%s"%(PULSE_DIRECTORY, year, PULSE_MONTHLY)
+
+ @staticmethod
+ def get_datas(selected_date):
+ """ Return the pulse filename according to the month selected """
+ result = []
+ slot_pulses = MonthlyCounter.load(MonthlyCounter.get_filename(selected_date))
+ for time_slot, months in slot_pulses.items():
+ result.append({"time_slot":time_slot,"months":months})
+ return result
+
+ @staticmethod
+ async def refresh():
+ """ Refresh the counters """
+ if MonthlyCounter.last_update[0] + 599 < time.time():
+ MonthlyCounter.next_update[0] = 0
+
+ @staticmethod
+ async def manage():
+ """ Rebuild all month files if necessary """
+ while True:
+ if MonthlyCounter.next_update[0] <= 0:
+ break
+ MonthlyCounter.next_update[0] -= 1
+ await uasyncio.sleep(1)
+
+ daily_to_update, monthly_to_update, filenames = await MonthlyCounter().get_updates()
+ await DailyCounter.update (filenames, daily_to_update)
+ await MonthlyCounter.update(filenames, monthly_to_update)
+ MonthlyCounter.next_update[0] = 28793
+ MonthlyCounter.last_update[0] = time.time()
+ return True
+
+ @staticmethod
+ async def task():
+ """ Task to count wh from the electric meter """
+ await tasking.task_monitoring(MonthlyCounter.manage)
+
+
Class variables
+
+
var force
+
+
+
+
var last_update
+
+
+
+
var next_update
+
+
+
+
+
Static methods
+
+
+def get_datas(selected_date)
+
+
+
Return the pulse filename according to the month selected
+
+
+Expand source code
+
+
@staticmethod
+def get_datas(selected_date):
+ """ Return the pulse filename according to the month selected """
+ result = []
+ slot_pulses = MonthlyCounter.load(MonthlyCounter.get_filename(selected_date))
+ for time_slot, months in slot_pulses.items():
+ result.append({"time_slot":time_slot,"months":months})
+ return result
+
+
+
+def get_filename(selected_date=None)
+
+
+
Return the filename according to day
+
+
+Expand source code
+
+
@staticmethod
+def get_filename(selected_date=None):
+ """ Return the filename according to day """
+ global PULSE_DIRECTORY, PULSE_MONTHLY
+ if selected_date is None:
+ selected_date = time.time()
+ year = date.local_time(selected_date)[0]
+ return "%s/%04d%s"%(PULSE_DIRECTORY, year, PULSE_MONTHLY)
+
+
+
+async def get_updates()
+
+
+
Build the list of daily and monthly file to update
+
+
+Expand source code
+
+
@staticmethod
+async def get_updates():
+ """ Build the list of daily and monthly file to update """
+ global PULSE_DIRECTORY, PULSE_MONTHLY, PULSE_DAILY
+ force = MonthlyCounter.force[0]
+ _, filenames = await filesystem.ascandir(PULSE_DIRECTORY, "*", True)
+ filenames.sort()
+ daily_to_update = {}
+ monthly_to_update = {}
+ for filename in filenames:
+ if fnmatch.fnmatch(filename, "*"+PULSE_HOURLY):
+ name = filesystem.splitext(filesystem.split(filename)[1])[0]
+ year, month, day = name.split("-")
+ daily = "%s/%s-%s/%s-%s%s"%(PULSE_DIRECTORY, year, month, year, month, PULSE_DAILY)
+ monthly = "%s/%s%s"%(PULSE_DIRECTORY, year, PULSE_MONTHLY)
+ update = False
+ if daily not in daily_to_update:
+ if filesystem.exists(daily):
+ daily_date = filesystem.fileinfo(daily)[8]
+ hourly_date = filesystem.fileinfo(filename)[8]
+ if hourly_date > daily_date or force:
+ update = True
+ else:
+ update = True
+ if update:
+ daily_to_update[(year, month)] = daily
+ monthly_to_update[year] = monthly
+ await uasyncio.sleep_ms(2)
+ MonthlyCounter.force[0] = False
+ return daily_to_update, monthly_to_update, filenames
+
+
+
+def load(filename)
+
+
+
Load daily file content
+
+
+Expand source code
+
+
@staticmethod
+def load(filename):
+ """ Load daily file content """
+ result = {}
+ try:
+ with open(filename, "rb") as file:
+ while True:
+ data = file.read(2)
+ if len(data) == 0:
+ break
+ start = struct.unpack("H", data)[0]*60
+ end = struct.unpack("H", file.read(2))[0]*60
+ months = struct.unpack("I"*12, file.read(4*12))
+ result[(start,end)] = months
+ except Exception as err:
+ logger.exception(err)
+ return result
class PulseSensor:
+ """ Detect wh pulse from electric meter """
+ def __init__(self, gpio, min_duration_ns=20_000_000, queue_length=1_500):
+ self.pulses = collections.deque((), queue_length)
+ self.notifier = uasyncio.Event()
+ self.previous_counter = 0
+ self.previous_time = time.time_ns()
+ self.min_duration_ns = min_duration_ns
+
+ # The use of pcnt counter allows to obtain a better reliability of counting
+ self.counter = machine.Counter(0, src=machine.Pin(gpio, mode=machine.Pin.IN), direction=machine.Counter.UP)
+ self.counter.filter_ns(self.min_duration_ns)
+ self.counter.pause()
+ self.counter.value(0)
+
+ self.sensor = machine.Pin(gpio, machine.Pin.IN, machine.Pin.PULL_DOWN)
+ self.sensor.irq(handler=self.detected, trigger=machine.Pin.IRQ_RISING)
+ self.counter.resume()
+
+ def __del__(self):
+ """ Destructor """
+ self.sensor.irq(handler=None)
+ self.counter.deinit()
+
+ def detected(self, pin):
+ """ Callback called when pulse detected """
+ pulse_time = time.time_ns()
+ pulse_counter = self.counter.value()
+ if pin.value() == 1:
+ # If new pulses detected
+ if pulse_counter != self.previous_counter:
+ # if the pulse is not too close to the previous one
+ if self.previous_time + self.min_duration_ns < pulse_time:
+ # If the new value is not a counter overflow
+ if pulse_counter > self.previous_counter:
+ pulse_quantity = pulse_counter - self.previous_counter
+ else:
+ pulse_quantity = 1
+
+ # Save the new counter value
+ self.previous_counter = pulse_counter
+
+ # Add the current pulse to list
+ self.pulses.append((pulse_quantity, pulse_time))
+
+ # Wake up pulse counter
+ self.notifier.set()
+ else:
+ # Ignore previous pulse
+ self.previous_counter = pulse_counter
+
+ async def wait(self):
+ """ Wait the wh pulses and the returns the list of pulses """
+ # Wait pulses
+ await self.notifier.wait()
+
+ # Clear notification flag
+ self.notifier.clear()
+ result = []
+
+ # Empty list of pulses
+ while True:
+ try:
+ result.append(self.pulses.popleft())
+ except IndexError:
+ break
+ return result
+
+ def simulate(self, pulses):
+ """ Simulates the flashing led on the electric meter """
+ self.sensor.value(1)
+ self.counter.value(pulses)
+ self.detected(self.sensor)
+
+
Methods
+
+
+def detected(self, pin)
+
+
+
Callback called when pulse detected
+
+
+Expand source code
+
+
def detected(self, pin):
+ """ Callback called when pulse detected """
+ pulse_time = time.time_ns()
+ pulse_counter = self.counter.value()
+ if pin.value() == 1:
+ # If new pulses detected
+ if pulse_counter != self.previous_counter:
+ # if the pulse is not too close to the previous one
+ if self.previous_time + self.min_duration_ns < pulse_time:
+ # If the new value is not a counter overflow
+ if pulse_counter > self.previous_counter:
+ pulse_quantity = pulse_counter - self.previous_counter
+ else:
+ pulse_quantity = 1
+
+ # Save the new counter value
+ self.previous_counter = pulse_counter
+
+ # Add the current pulse to list
+ self.pulses.append((pulse_quantity, pulse_time))
+
+ # Wake up pulse counter
+ self.notifier.set()
+ else:
+ # Ignore previous pulse
+ self.previous_counter = pulse_counter
+
+
+
+def simulate(self, pulses)
+
+
+
Simulates the flashing led on the electric meter
+
+
+Expand source code
+
+
def simulate(self, pulses):
+ """ Simulates the flashing led on the electric meter """
+ self.sensor.value(1)
+ self.counter.value(pulses)
+ self.detected(self.sensor)
+
+
+
+async def wait(self)
+
+
+
Wait the wh pulses and the returns the list of pulses
+
+
+Expand source code
+
+
async def wait(self):
+ """ Wait the wh pulses and the returns the list of pulses """
+ # Wait pulses
+ await self.notifier.wait()
+
+ # Clear notification flag
+ self.notifier.clear()
+ result = []
+
+ # Empty list of pulses
+ while True:
+ try:
+ result.append(self.pulses.popleft())
+ except IndexError:
+ break
+ return result
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/electricmeter/em_lang.html b/doc/lib/electricmeter/em_lang.html
new file mode 100644
index 0000000..134c40a
--- /dev/null
+++ b/doc/lib/electricmeter/em_lang.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+lib.electricmeter.em_lang API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module lib.electricmeter.em_lang
+
+
+
Language selected and regional time
+
+
+Expand source code
+
+
# Distributed under MIT License
+# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
+""" Language selected and regional time """
+from tools import region, strings, logger
+
+try:
+ exec(b"from electricmeter.em_lang_%s import *"%region.RegionConfig.get().lang)
+ logger.syslog("Select electricmeter lang : %s"%strings.tostrings(region.RegionConfig.get().lang))
+except Exception as err:
+ logger.syslog(err)
+ from electricmeter.em_lang_english import *
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/electricmeter/em_lang_english.html b/doc/lib/electricmeter/em_lang_english.html
new file mode 100644
index 0000000..9389f6d
--- /dev/null
+++ b/doc/lib/electricmeter/em_lang_english.html
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+lib.electricmeter.em_lang_english API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module lib.electricmeter.em_lang_english
+
+
+
English text for electricmeter
+
+
+Expand source code
+
+
# Distributed under MIT License
+# Copyright (c) 2021 Remi BERTHOLET
+""" English text for electricmeter """
+title_electricmeter = b"Power consumption per "
+menu_electricmeter = b"Electric meter"
+
+rate = b"Rate"
+field_rate = b"Name defining the rate"
+price = b"Price per kWh"
+currency = b"Currency"
+field_currency = b"Currency to be used for the fare"
+validy_date = b"Validity date"
+step_minutes = b"minutes"
+name = b"Name"
+type_price = b"Prix"
+type_power = b"kWh"
+
+title_rate = b"Rate per kWh"
+add_button = b"Add"
+remove_button = b"\xF0\x9F\x97\x91"
+from_the = b"per kWh, "
+remove_dialog = b"do you want to delete ?"
+
+item_time_slots = b"Time slots"
+title_time_slots = b"Time slots"
+field_start = b"Start time"
+field_end = b"End time"
+field_time_rate = b"Rate name"
+field_color = b"Rate color"
+
+item_hour = b"Hour"
+item_day = b"Day"
+item_month = b"Month"
+item_year = b"Year"
+item_rate = b"Rate"
+power_consumed = b"Instantaneous power consumption"
+
+item_geolocation = b"Geolocation"
+latitude = b"Latitude"
+longitude = b"Longitude"
+temperature = b"Temperature"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/electricmeter/em_lang_french.html b/doc/lib/electricmeter/em_lang_french.html
new file mode 100644
index 0000000..ff68fc1
--- /dev/null
+++ b/doc/lib/electricmeter/em_lang_french.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+lib.electricmeter.em_lang_french API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Module lib.electricmeter.em_lang_french
+
+
+
Textes en francais pour watt metre
+
+
+Expand source code
+
+
# Distributed under MIT License
+# Copyright (c) 2021 Remi BERTHOLET
+""" Textes en francais pour watt metre """
+title_electricmeter = b"Puissance consomm\xC3\xA9e par "
+menu_electricmeter = b"Compteur"
+
+rate = b"Tarif"
+field_rate = b"Nom d\xC3\xA9finissant le tarif"
+price = b"Prix au kwh"
+currency = b"Devise"
+field_currency = b"Devise \xc3\xa0 utiliser pour le tarif"
+validy_date = b"Date de validit\xC3\xA9"
+step_minutes = b"minutes"
+name = b"Nom"
+type_price = b"Prix"
+type_power = b"kWh"
+
+title_rate = b"Tarif du kWh"
+add_button = b"Ajouter"
+remove_button = b"\xF0\x9F\x97\x91"
+from_the = b"par kWh, "
+remove_dialog = b"Voulez vous supprimer ?"
+
+item_time_slots = b"Plages horaire"
+title_time_slots = b"Plages horaire"
+field_start = b"Heure de d\xC3\xA9but"
+field_end = b"Heure de fin"
+field_time_rate = b"Nom du tarif"
+field_color = b"Couleur du tarif"
+
+item_hour = b"Heure"
+item_day = b"Jour"
+item_month = b"Mois"
+item_year = b"Ann\xC3\xA9e"
+item_rate = b"Tarif"
+
+power_consumed = b"Puissance instantan\xC3\xA9e"
+
+item_geolocation = b"G\xC3\xA9olocalisation"
+latitude = b"Latitude"
+longitude = b"Longitude"
+temperature = b"Temp\xC3\xA9rature"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/electricmeter/htmlpage.html b/doc/lib/electricmeter/htmlpage.html
new file mode 100644
index 0000000..2493ee2
--- /dev/null
+++ b/doc/lib/electricmeter/htmlpage.html
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+lib.electricmeter.htmlpage API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
""" This script parse the template.html file and creates the template classes
that can be used to compose a web page.
This automatically creates the content of file lib/htmltemplate/htmlclasses.py """
+# pylint:disable=consider-using-f-string
import re
def findall(pattern, text):
@@ -60,7 +61,7 @@
Module lib.htmltemplate.htmlparser
elif end_tag is not None:
end_tag += s
if var != "":
- if var in ["disabled","checked","active","selected"]:
+ if var in ["disabled","checked","active","selected","required","novalidate"]:
end_format += " b'%s' if self.%s else b'',"%(var, var)
else:
end_format += "self.%s,"%var
@@ -68,7 +69,7 @@
Module lib.htmltemplate.htmlparser
else:
begin_tag += s
if var != "":
- if var in ["disabled","checked","active","selected"]:
+ if var in ["disabled","checked","active","selected","required","novalidate"]:
begin_format += " b'%s' if self.%s else b'',"%(var, var)
else:
begin_format += "self.%s,"%var
@@ -83,10 +84,11 @@
Module lib.htmltemplate.htmlparser
""" Parse the www/template.html and createsthe content of file lib/htmltemplate/htmlclasses.py """
from htmltemplate import WWW_DIR, TEMPLATE_FILE, TEMPLATE_PY
# pylint: disable=duplicate-string-formatting-argument
+ # pylint:disable=unspecified-encoding
print("Parse html template")
lines = open(WWW_DIR+TEMPLATE_FILE).readlines()
py_class_file = open(TEMPLATE_PY,"w")
- py_class_file.write("''' File automatically generated with template.html content '''\n# pylint:disable=missing-function-docstring\n# pylint:disable=trailing-whitespace\n# pylint:disable=too-many-lines\nfrom htmltemplate.template import Template \n")
+ py_class_file.write("''' File automatically generated with template.html content '''\n# pylint:disable=missing-function-docstring\n# pylint:disable=global-variable-not-assigned\n# pylint:disable=trailing-whitespace\n# pylint:disable=too-many-lines\nfrom htmltemplate.template import Template \n")
stack = []
for line in lines:
@@ -148,12 +150,13 @@
elif end_tag is not None:
end_tag += s
if var != "":
- if var in ["disabled","checked","active","selected"]:
+ if var in ["disabled","checked","active","selected","required","novalidate"]:
end_format += " b'%s' if self.%s else b'',"%(var, var)
else:
end_format += "self.%s,"%var
@@ -212,7 +215,7 @@
Functions
else:
begin_tag += s
if var != "":
- if var in ["disabled","checked","active","selected"]:
+ if var in ["disabled","checked","active","selected","required","novalidate"]:
begin_format += " b'%s' if self.%s else b'',"%(var, var)
else:
begin_format += "self.%s,"%var
@@ -237,10 +240,11 @@
Functions
""" Parse the www/template.html and createsthe content of file lib/htmltemplate/htmlclasses.py """
from htmltemplate import WWW_DIR, TEMPLATE_FILE, TEMPLATE_PY
# pylint: disable=duplicate-string-formatting-argument
+ # pylint:disable=unspecified-encoding
print("Parse html template")
lines = open(WWW_DIR+TEMPLATE_FILE).readlines()
py_class_file = open(TEMPLATE_PY,"w")
- py_class_file.write("''' File automatically generated with template.html content '''\n# pylint:disable=missing-function-docstring\n# pylint:disable=trailing-whitespace\n# pylint:disable=too-many-lines\nfrom htmltemplate.template import Template \n")
+ py_class_file.write("''' File automatically generated with template.html content '''\n# pylint:disable=missing-function-docstring\n# pylint:disable=global-variable-not-assigned\n# pylint:disable=trailing-whitespace\n# pylint:disable=too-many-lines\nfrom htmltemplate.template import Template \n")
stack = []
for line in lines:
@@ -302,12 +306,13 @@
class Template:
""" Base class of html templates """
+ default_spacer = b"mb-3"
def __init__(self, classname, *args, **params):
+ """ """
self.classname = classname
self.children = []
+ self.spacer = b""
if len(args) > 0:
children = args[0]
else:
@@ -44,6 +47,11 @@
Module lib.htmltemplate.template
if children != []:
self.add_children(children)
+ def end_init(self, **params):
+ """ Terminate initialisation"""
+ if params.get("spacer", None) is None:
+ self.spacer = Template.default_spacer
+
def add_children(self, children):
""" Add children of html template in the current instance """
if type(children) == type([]) or type(children) == type((0,)):
@@ -93,9 +101,12 @@
Classes
class Template:
""" Base class of html templates """
+ default_spacer = b"mb-3"
def __init__(self, classname, *args, **params):
+ """ """
self.classname = classname
self.children = []
+ self.spacer = b""
if len(args) > 0:
children = args[0]
else:
@@ -103,6 +114,11 @@
Classes
if children != []:
self.add_children(children)
+ def end_init(self, **params):
+ """ Terminate initialisation"""
+ if params.get("spacer", None) is None:
+ self.spacer = Template.default_spacer
+
def add_children(self, children):
""" Add children of html template in the current instance """
if type(children) == type([]) or type(children) == type((0,)):
@@ -130,6 +146,13 @@
Classes
except Exception as err:
await file.write(logger.html_exception(err))
+
Class variables
+
+
var default_spacer
+
+
+
+
Methods
@@ -150,6 +173,21 @@
Methods
self.children.append(children)
+
+def end_init(self, **params)
+
+
+
Terminate initialisation
+
+
+Expand source code
+
+
def end_init(self, **params):
+ """ Terminate initialisation"""
+ if params.get("spacer", None) is None:
+ self.spacer = Template.default_spacer
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Manage the motion detection history file """
import re
import json
@@ -36,9 +37,9 @@
Module lib.motion.historic
import uos
from tools import logger,sdcard,tasking,filesystem,strings,info
-MAX_DAYS_DISPLAYED = 21
+MAX_DAYS_DISPLAYED = 28
MAX_DAYS_REMOVED = 14
-MAX_MOTIONS = MAX_DAYS_DISPLAYED*50
+MAX_MOTIONS = 400
class Historic:
""" Manage the motion detection history file """
@@ -81,7 +82,7 @@
Module lib.motion.historic
name = strings.tostrings(name)
item = Historic.create_item(root + "/" + path + "/" + name +".json", motion_info)
res1 = sdcard.SdCard.save(path, name + ".jpg" , image)
- res2 = sdcard.SdCard.save(path, name + ".json", json.dumps(item))
+ res2 = sdcard.SdCard.save(path, name + ".json", json.dumps(item, separators=(',', ':')))
Historic.add_item(item)
result = res1 and res2
except Exception as err:
@@ -108,6 +109,26 @@
Module lib.motion.historic
# Remove the "/" before filename
item [0] = item [0].lstrip("/")
+ # If the differences are in an old format
+ if type(item[3]) == type(""):
+ diffs = []
+ diff_val = 0
+ i = 0
+ for diff in item[3]:
+ if diff == "#":
+ diff_val |= 1
+
+ if (i %32 == 31):
+ diffs.append(diff_val)
+ diff_val = 0
+ else:
+ diff_val <<= 1
+ i += 1
+ diff_max = len(item[3])
+ diff_val <<= (31 - (diff_max%32))
+ diffs.append(diff_val)
+ item[3] = diffs
+
# Add json file to the historic
Historic.historic.insert(0,item)
@@ -119,7 +140,7 @@
Module lib.motion.historic
try:
await Historic.acquire()
Historic.historic.clear()
-
+ last_day = ""
# For all motions
for motion in motions:
try:
@@ -132,6 +153,9 @@
Module lib.motion.historic
filename = filename.lstrip("/")
if filesystem.exists(filename):
Historic.add_item(motion_item)
+ if last_day != motion_item[0][4:14]:
+ last_day = motion_item[0][4:14]
+ print("Build historic day %s"%last_day)
except OSError as err:
logger.syslog(err)
# If sd card not responding properly
@@ -142,7 +166,8 @@
Module lib.motion.historic
finally:
if file:
file.close()
- await uasyncio.sleep_ms(3)
+ if filesystem.ismicropython():
+ await uasyncio.sleep_ms(2)
except Exception as err:
logger.syslog(err)
finally:
@@ -152,15 +177,14 @@
Module lib.motion.historic
async def get_json():
""" Read the historic from disk """
root = Historic.get_root()
- result = b""
+ result = b"[]"
if root:
+ await Historic.reduce_history()
try:
await Historic.acquire()
Historic.historic.sort()
Historic.historic.reverse()
- while len(Historic.historic) > MAX_MOTIONS:
- del Historic.historic[-1]
- result = strings.tobytes(json.dumps(Historic.historic))
+ result = strings.tobytes(json.dumps(Historic.historic, separators=(',', ':')))
except Exception as err:
logger.syslog(err)
finally:
@@ -184,7 +208,7 @@
Module lib.motion.historic
for day in lastdays:
logger.syslog(" %s"%day)
logger.syslog("End historic creation")
- logger.syslog(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False)))
+ logger.syslog(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint())))
except Exception as err:
logger.syslog(err)
@@ -203,7 +227,8 @@
Module lib.motion.historic
if typ & 0xF000 != 0x4000:
if re.match(pattern, name):
result.append(name)
- await uasyncio.sleep_ms(5)
+ if filesystem.ismicropython():
+ await uasyncio.sleep_ms(3)
result.sort()
if older is False:
result.reverse()
@@ -220,29 +245,31 @@
Module lib.motion.historic
await Historic.acquire()
years = await Historic.scan_dir(root, r"\d\d\d\d", older)
for year in years:
- pathYear = root + "/" + year
- months = await Historic.scan_dir(pathYear, r"\d\d", older)
+ path_year = root + "/" + year
+ months = await Historic.scan_dir(path_year, r"\d\d", older)
for month in months:
- pathMonth = pathYear + "/" + month
- days = await Historic.scan_dir(pathMonth, r"\d\d", older)
+ path_month = path_year + "/" + month
+ days = await Historic.scan_dir(path_month, r"\d\d", older)
for day in days:
- pathDay = pathMonth + "/" + day
- hours = await Historic.scan_dir(pathDay, r"\d\dh\d\d", older)
+ print("Scan historic day %s/%s/%s"%(year, month, day))
+ path_day = path_month + "/" + day
+ hours = await Historic.scan_dir(path_day, r"\d\dh\d\d", older)
lastdays.append("%s/%s/%s"%(year, month, day))
- if len(lastdays) > max_days or len(motions) > MAX_MOTIONS:
- motions.sort()
- if older is False:
- motions.reverse()
- return motions, lastdays
+
for hour in hours:
- pathHour = pathDay + "/" + hour
+ path_hour = path_day + "/" + hour
if older:
extension = "jpg"
else:
extension = "json"
- detections = await Historic.scan_dir(pathHour, r"\d\d.*\."+extension, older, directory=False)
+ detections = await Historic.scan_dir(path_hour, r"\d\d.*\."+extension, older, directory=False)
for detection in detections:
- motions.append(pathHour + "/" + detection)
+ if len(lastdays) > max_days or len(motions) > MAX_MOTIONS:
+ motions.sort()
+ if older is False:
+ motions.reverse()
+ return motions, lastdays
+ motions.append(path_hour + "/" + detection)
motions.sort()
if older is False:
motions.reverse()
@@ -293,17 +320,47 @@
Module lib.motion.historic
else:
shell.rmdir(directory, recursive=True, simulate=simulate, force=force)
+ @staticmethod
+ async def reduce_history():
+ """ Reduce the history length """
+ try:
+ await Historic.acquire()
+
+ if len(Historic.historic) > MAX_MOTIONS:
+ while len(Historic.historic) > MAX_MOTIONS:
+ del Historic.historic[-1]
+
+ finally:
+ await Historic.release()
+
+ @staticmethod
+ async def get_last_days():
+ """ Return the list of last days """
+ last_days = set()
+ try:
+ await Historic.acquire()
+ for item in Historic.historic:
+ filename = item[0].lstrip("/").split("/")
+ date_ = b"%s/%s/%s"%(strings.tobytes(filename[1]),strings.tobytes(filename[2]),strings.tobytes(filename[3]))
+ last_days.add(date_)
+ finally:
+ await Historic.release()
+ return last_days
+
@staticmethod
async def remove_older(force=False):
""" Remove older files to make space """
root = Historic.get_root()
if root:
+ await Historic.reduce_history()
+
# If not enough space available on sdcard
if sdcard.SdCard.is_not_enough_space(low=True) or force:
logger.syslog("Start cleanup historic")
Historic.first_extract[0] = False
olders, lastdays = await Historic.scan_directories(MAX_DAYS_REMOVED, True)
previous = ""
+
for motion in olders:
try:
await Historic.acquire()
@@ -317,16 +374,16 @@
Module lib.motion.historic
await Historic.release()
if sdcard.SdCard.is_not_enough_space(low=False) is False:
break
- logger.syslog("End cleanup historic : %s"%(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False))))
+ logger.syslog("End cleanup historic : %s"%(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint()))))
@staticmethod
async def periodic():
""" Internal periodic task """
from server.server import Server
if filesystem.ismicropython():
- await Server.wait_resume(241)
+ await Server.wait_resume(31)
else:
- await Server.wait_resume(3)
+ await Server.wait_resume(1)
if Historic.motion_in_progress[0] is False:
if sdcard.SdCard.is_mounted() is False:
@@ -407,7 +464,7 @@
Classes
name = strings.tostrings(name)
item = Historic.create_item(root + "/" + path + "/" + name +".json", motion_info)
res1 = sdcard.SdCard.save(path, name + ".jpg" , image)
- res2 = sdcard.SdCard.save(path, name + ".json", json.dumps(item))
+ res2 = sdcard.SdCard.save(path, name + ".json", json.dumps(item, separators=(',', ':')))
Historic.add_item(item)
result = res1 and res2
except Exception as err:
@@ -434,6 +491,26 @@
Classes
# Remove the "/" before filename
item [0] = item [0].lstrip("/")
+ # If the differences are in an old format
+ if type(item[3]) == type(""):
+ diffs = []
+ diff_val = 0
+ i = 0
+ for diff in item[3]:
+ if diff == "#":
+ diff_val |= 1
+
+ if (i %32 == 31):
+ diffs.append(diff_val)
+ diff_val = 0
+ else:
+ diff_val <<= 1
+ i += 1
+ diff_max = len(item[3])
+ diff_val <<= (31 - (diff_max%32))
+ diffs.append(diff_val)
+ item[3] = diffs
+
# Add json file to the historic
Historic.historic.insert(0,item)
@@ -445,7 +522,7 @@
Classes
try:
await Historic.acquire()
Historic.historic.clear()
-
+ last_day = ""
# For all motions
for motion in motions:
try:
@@ -458,6 +535,9 @@
Classes
filename = filename.lstrip("/")
if filesystem.exists(filename):
Historic.add_item(motion_item)
+ if last_day != motion_item[0][4:14]:
+ last_day = motion_item[0][4:14]
+ print("Build historic day %s"%last_day)
except OSError as err:
logger.syslog(err)
# If sd card not responding properly
@@ -468,7 +548,8 @@
Classes
finally:
if file:
file.close()
- await uasyncio.sleep_ms(3)
+ if filesystem.ismicropython():
+ await uasyncio.sleep_ms(2)
except Exception as err:
logger.syslog(err)
finally:
@@ -478,15 +559,14 @@
Classes
async def get_json():
""" Read the historic from disk """
root = Historic.get_root()
- result = b""
+ result = b"[]"
if root:
+ await Historic.reduce_history()
try:
await Historic.acquire()
Historic.historic.sort()
Historic.historic.reverse()
- while len(Historic.historic) > MAX_MOTIONS:
- del Historic.historic[-1]
- result = strings.tobytes(json.dumps(Historic.historic))
+ result = strings.tobytes(json.dumps(Historic.historic, separators=(',', ':')))
except Exception as err:
logger.syslog(err)
finally:
@@ -510,7 +590,7 @@
Classes
for day in lastdays:
logger.syslog(" %s"%day)
logger.syslog("End historic creation")
- logger.syslog(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False)))
+ logger.syslog(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint())))
except Exception as err:
logger.syslog(err)
@@ -529,7 +609,8 @@
Classes
if typ & 0xF000 != 0x4000:
if re.match(pattern, name):
result.append(name)
- await uasyncio.sleep_ms(5)
+ if filesystem.ismicropython():
+ await uasyncio.sleep_ms(3)
result.sort()
if older is False:
result.reverse()
@@ -546,29 +627,31 @@
Classes
await Historic.acquire()
years = await Historic.scan_dir(root, r"\d\d\d\d", older)
for year in years:
- pathYear = root + "/" + year
- months = await Historic.scan_dir(pathYear, r"\d\d", older)
+ path_year = root + "/" + year
+ months = await Historic.scan_dir(path_year, r"\d\d", older)
for month in months:
- pathMonth = pathYear + "/" + month
- days = await Historic.scan_dir(pathMonth, r"\d\d", older)
+ path_month = path_year + "/" + month
+ days = await Historic.scan_dir(path_month, r"\d\d", older)
for day in days:
- pathDay = pathMonth + "/" + day
- hours = await Historic.scan_dir(pathDay, r"\d\dh\d\d", older)
+ print("Scan historic day %s/%s/%s"%(year, month, day))
+ path_day = path_month + "/" + day
+ hours = await Historic.scan_dir(path_day, r"\d\dh\d\d", older)
lastdays.append("%s/%s/%s"%(year, month, day))
- if len(lastdays) > max_days or len(motions) > MAX_MOTIONS:
- motions.sort()
- if older is False:
- motions.reverse()
- return motions, lastdays
+
for hour in hours:
- pathHour = pathDay + "/" + hour
+ path_hour = path_day + "/" + hour
if older:
extension = "jpg"
else:
extension = "json"
- detections = await Historic.scan_dir(pathHour, r"\d\d.*\."+extension, older, directory=False)
+ detections = await Historic.scan_dir(path_hour, r"\d\d.*\."+extension, older, directory=False)
for detection in detections:
- motions.append(pathHour + "/" + detection)
+ if len(lastdays) > max_days or len(motions) > MAX_MOTIONS:
+ motions.sort()
+ if older is False:
+ motions.reverse()
+ return motions, lastdays
+ motions.append(path_hour + "/" + detection)
motions.sort()
if older is False:
motions.reverse()
@@ -619,17 +702,47 @@
Classes
else:
shell.rmdir(directory, recursive=True, simulate=simulate, force=force)
+ @staticmethod
+ async def reduce_history():
+ """ Reduce the history length """
+ try:
+ await Historic.acquire()
+
+ if len(Historic.historic) > MAX_MOTIONS:
+ while len(Historic.historic) > MAX_MOTIONS:
+ del Historic.historic[-1]
+
+ finally:
+ await Historic.release()
+
+ @staticmethod
+ async def get_last_days():
+ """ Return the list of last days """
+ last_days = set()
+ try:
+ await Historic.acquire()
+ for item in Historic.historic:
+ filename = item[0].lstrip("/").split("/")
+ date_ = b"%s/%s/%s"%(strings.tobytes(filename[1]),strings.tobytes(filename[2]),strings.tobytes(filename[3]))
+ last_days.add(date_)
+ finally:
+ await Historic.release()
+ return last_days
+
@staticmethod
async def remove_older(force=False):
""" Remove older files to make space """
root = Historic.get_root()
if root:
+ await Historic.reduce_history()
+
# If not enough space available on sdcard
if sdcard.SdCard.is_not_enough_space(low=True) or force:
logger.syslog("Start cleanup historic")
Historic.first_extract[0] = False
olders, lastdays = await Historic.scan_directories(MAX_DAYS_REMOVED, True)
previous = ""
+
for motion in olders:
try:
await Historic.acquire()
@@ -643,16 +756,16 @@
Classes
await Historic.release()
if sdcard.SdCard.is_not_enough_space(low=False) is False:
break
- logger.syslog("End cleanup historic : %s"%(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False))))
+ logger.syslog("End cleanup historic : %s"%(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint()))))
@staticmethod
async def periodic():
""" Internal periodic task """
from server.server import Server
if filesystem.ismicropython():
- await Server.wait_resume(241)
+ await Server.wait_resume(31)
else:
- await Server.wait_resume(3)
+ await Server.wait_resume(1)
if Historic.motion_in_progress[0] is False:
if sdcard.SdCard.is_mounted() is False:
@@ -726,6 +839,26 @@
Static methods
# Remove the "/" before filename
item [0] = item [0].lstrip("/")
+ # If the differences are in an old format
+ if type(item[3]) == type(""):
+ diffs = []
+ diff_val = 0
+ i = 0
+ for diff in item[3]:
+ if diff == "#":
+ diff_val |= 1
+
+ if (i %32 == 31):
+ diffs.append(diff_val)
+ diff_val = 0
+ else:
+ diff_val <<= 1
+ i += 1
+ diff_max = len(item[3])
+ diff_val <<= (31 - (diff_max%32))
+ diffs.append(diff_val)
+ item[3] = diffs
+
# Add json file to the historic
Historic.historic.insert(0,item)
@@ -751,7 +884,7 @@
Static methods
name = strings.tostrings(name)
item = Historic.create_item(root + "/" + path + "/" + name +".json", motion_info)
res1 = sdcard.SdCard.save(path, name + ".jpg" , image)
- res2 = sdcard.SdCard.save(path, name + ".json", json.dumps(item))
+ res2 = sdcard.SdCard.save(path, name + ".json", json.dumps(item, separators=(',', ':')))
Historic.add_item(item)
result = res1 and res2
except Exception as err:
@@ -778,7 +911,7 @@
Static methods
try:
await Historic.acquire()
Historic.historic.clear()
-
+ last_day = ""
# For all motions
for motion in motions:
try:
@@ -791,6 +924,9 @@
Static methods
filename = filename.lstrip("/")
if filesystem.exists(filename):
Historic.add_item(motion_item)
+ if last_day != motion_item[0][4:14]:
+ last_day = motion_item[0][4:14]
+ print("Build historic day %s"%last_day)
except OSError as err:
logger.syslog(err)
# If sd card not responding properly
@@ -801,7 +937,8 @@
Static methods
finally:
if file:
file.close()
- await uasyncio.sleep_ms(3)
+ if filesystem.ismicropython():
+ await uasyncio.sleep_ms(2)
except Exception as err:
logger.syslog(err)
finally:
@@ -854,7 +991,7 @@
Static methods
for day in lastdays:
logger.syslog(" %s"%day)
logger.syslog("End historic creation")
- logger.syslog(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False)))
+ logger.syslog(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint())))
except Exception as err:
logger.syslog(err)
@@ -872,15 +1009,14 @@
Static methods
async def get_json():
""" Read the historic from disk """
root = Historic.get_root()
- result = b""
+ result = b"[]"
if root:
+ await Historic.reduce_history()
try:
await Historic.acquire()
Historic.historic.sort()
Historic.historic.reverse()
- while len(Historic.historic) > MAX_MOTIONS:
- del Historic.historic[-1]
- result = strings.tobytes(json.dumps(Historic.historic))
+ result = strings.tobytes(json.dumps(Historic.historic, separators=(',', ':')))
except Exception as err:
logger.syslog(err)
finally:
@@ -888,6 +1024,30 @@
Static methods
return result
+
+async def get_last_days()
+
+
+
Return the list of last days
+
+
+Expand source code
+
+
@staticmethod
+async def get_last_days():
+ """ Return the list of last days """
+ last_days = set()
+ try:
+ await Historic.acquire()
+ for item in Historic.historic:
+ filename = item[0].lstrip("/").split("/")
+ date_ = b"%s/%s/%s"%(strings.tobytes(filename[1]),strings.tobytes(filename[2]),strings.tobytes(filename[3]))
+ last_days.add(date_)
+ finally:
+ await Historic.release()
+ return last_days
+
+
def get_root()
@@ -934,9 +1094,9 @@
Static methods
""" Internal periodic task """
from server.server import Server
if filesystem.ismicropython():
- await Server.wait_resume(241)
+ await Server.wait_resume(31)
else:
- await Server.wait_resume(3)
+ await Server.wait_resume(1)
if Historic.motion_in_progress[0] is False:
if sdcard.SdCard.is_mounted() is False:
@@ -962,6 +1122,29 @@
@staticmethod
+async def reduce_history():
+ """ Reduce the history length """
+ try:
+ await Historic.acquire()
+
+ if len(Historic.historic) > MAX_MOTIONS:
+ while len(Historic.historic) > MAX_MOTIONS:
+ del Historic.historic[-1]
+
+ finally:
+ await Historic.release()
+
+
async def release()
@@ -1037,12 +1220,15 @@
Static methods
""" Remove older files to make space """
root = Historic.get_root()
if root:
+ await Historic.reduce_history()
+
# If not enough space available on sdcard
if sdcard.SdCard.is_not_enough_space(low=True) or force:
logger.syslog("Start cleanup historic")
Historic.first_extract[0] = False
olders, lastdays = await Historic.scan_directories(MAX_DAYS_REMOVED, True)
previous = ""
+
for motion in olders:
try:
await Historic.acquire()
@@ -1056,7 +1242,7 @@
Static methods
await Historic.release()
if sdcard.SdCard.is_not_enough_space(low=False) is False:
break
- logger.syslog("End cleanup historic : %s"%(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False))))
+ logger.syslog("End cleanup historic : %s"%(strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint()))))
@@ -1083,7 +1269,8 @@
Static methods
if typ & 0xF000 != 0x4000:
if re.match(pattern, name):
result.append(name)
- await uasyncio.sleep_ms(5)
+ if filesystem.ismicropython():
+ await uasyncio.sleep_ms(3)
result.sort()
if older is False:
result.reverse()
@@ -1110,29 +1297,31 @@
Static methods
await Historic.acquire()
years = await Historic.scan_dir(root, r"\d\d\d\d", older)
for year in years:
- pathYear = root + "/" + year
- months = await Historic.scan_dir(pathYear, r"\d\d", older)
+ path_year = root + "/" + year
+ months = await Historic.scan_dir(path_year, r"\d\d", older)
for month in months:
- pathMonth = pathYear + "/" + month
- days = await Historic.scan_dir(pathMonth, r"\d\d", older)
+ path_month = path_year + "/" + month
+ days = await Historic.scan_dir(path_month, r"\d\d", older)
for day in days:
- pathDay = pathMonth + "/" + day
- hours = await Historic.scan_dir(pathDay, r"\d\dh\d\d", older)
+ print("Scan historic day %s/%s/%s"%(year, month, day))
+ path_day = path_month + "/" + day
+ hours = await Historic.scan_dir(path_day, r"\d\dh\d\d", older)
lastdays.append("%s/%s/%s"%(year, month, day))
- if len(lastdays) > max_days or len(motions) > MAX_MOTIONS:
- motions.sort()
- if older is False:
- motions.reverse()
- return motions, lastdays
+
for hour in hours:
- pathHour = pathDay + "/" + hour
+ path_hour = path_day + "/" + hour
if older:
extension = "jpg"
else:
extension = "json"
- detections = await Historic.scan_dir(pathHour, r"\d\d.*\."+extension, older, directory=False)
+ detections = await Historic.scan_dir(path_hour, r"\d\d.*\."+extension, older, directory=False)
for detection in detections:
- motions.append(pathHour + "/" + detection)
+ if len(lastdays) > max_days or len(motions) > MAX_MOTIONS:
+ motions.sort()
+ if older is False:
+ motions.reverse()
+ return motions, lastdays
+ motions.append(path_hour + "/" + detection)
motions.sort()
if older is False:
motions.reverse()
@@ -1203,6 +1392,7 @@
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Motion detection only work with ESP32CAM (Requires specially modified ESP32CAM firmware to handle motion detection.) """
from gc import collect
import sys
@@ -40,7 +41,7 @@
Module lib.motion.motion
from server.presence import Presence
from motion.historic import Historic
from video.video import Camera
-from tools import logger,jsonconfig,lang,linearfunction,tasking,strings,filesystem
+from tools import logger,jsonconfig,lang,linearfunction,tasking,strings,filesystem,date
class MotionConfig(jsonconfig.JsonConfig):
""" Configuration class of motion detection """
@@ -99,9 +100,9 @@
index = image.index
diffs += b"%d:%d%s%s"%(image.get_motion_id(), image.get_diff_count(), (0x41 + ((256-image.get_diff_histo())//10)).to_bytes(1, 'big'), trace)
if display:
- line = b"\r%s %s L%d (%d) "%(strings.date_to_bytes()[12:], bytes(diffs), mean_light, index)
+ line = b"\r%s %s L%d (%d) "%(date.time_to_html(), bytes(diffs), mean_light, index)
if filesystem.ismicropython():
sys.stdout.write(line)
else:
@@ -2192,7 +2193,7 @@
Methods
index = image.index
diffs += b"%d:%d%s%s"%(image.get_motion_id(), image.get_diff_count(), (0x41 + ((256-image.get_diff_histo())//10)).to_bytes(1, 'big'), trace)
if display:
- line = b"\r%s %s L%d (%d) "%(strings.date_to_bytes()[12:], bytes(diffs), mean_light, index)
+ line = b"\r%s %s L%d (%d) "%(date.time_to_html(), bytes(diffs), mean_light, index)
if filesystem.ismicropython():
sys.stdout.write(line)
else:
diff --git a/doc/lib/server/dnsclient.html b/doc/lib/server/dnsclient.html
index e4367dc..8fb6fe2 100644
--- a/doc/lib/server/dnsclient.html
+++ b/doc/lib/server/dnsclient.html
@@ -31,6 +31,7 @@
Module lib.server.dnsclient
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
# DNS spec https://www2.cs.duke.edu/courses/fall16/compsci356/DNS/DNS-primer.pdf
+# pylint:disable=consider-using-f-string
import struct
import random
import socket
diff --git a/doc/lib/server/ftpserver.html b/doc/lib/server/ftpserver.html
index 1b91e35..0f6e598 100644
--- a/doc/lib/server/ftpserver.html
+++ b/doc/lib/server/ftpserver.html
@@ -38,6 +38,7 @@
Module lib.server.ftpserver
# historically based on :
# https://github.com/robert-hh/FTP-Server-for-ESP8266-ESP32-and-PYBD/blob/master/ftp.py
# but I have modified a lot, there must still be some original functions.
+# pylint:disable=consider-using-f-string
""" Ftp server main class.
This class contains few lines of code, this is to save memory.
The core of the server is in the other class FtpServerCore, which is loaded into memory only when connecting an FTP client.
diff --git a/doc/lib/server/ftpservercore.html b/doc/lib/server/ftpservercore.html
index dec2e59..4bc822f 100644
--- a/doc/lib/server/ftpservercore.html
+++ b/doc/lib/server/ftpservercore.html
@@ -32,6 +32,8 @@
Module lib.server.ftpservercore
# historically based on :
# https://github.com/robert-hh/FTP-Server-for-ESP8266-ESP32-and-PYBD/blob/master/ftp.py
# but I have modified a lot, there must still be some original functions.
+# pylint:disable=consider-using-f-string
+# pylint:disable=unspecified-encoding
""" Ftp server implementation core class """
import socket
import os
@@ -41,7 +43,7 @@
Module lib.server.ftpservercore
from server.user import User
from wifi.accesspoint import AccessPoint
from wifi.station import Station
-from tools import logger,fnmatch,filesystem,strings
+from tools import logger,fnmatch,filesystem,strings,date
MONTHS = [b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov", b"Dec"]
@@ -79,7 +81,12 @@
Module lib.server.ftpservercore
self.received = None
self.remoteaddr = None
self.client = None
- logger.syslog(b"[FTP] Open data %d"%self.dataport)
+ self.log(b"Open data %d"%self.dataport)
+
+ def log(self, err, msg="", write=False):
+ """ Log message """
+ if write:
+ logger.syslog(err, msg=msg, write=write)
def get_ip(self):
""" Get the ip address of the board """
@@ -100,12 +107,12 @@
Module lib.server.ftpservercore
""" Destroy ftp instance """
self.close()
- def get_file_description(self, filename, typ, size, date, now, full):
+ def get_file_description(self, filename, typ, size, date_, now, full):
""" Build list of file description """
if full:
file_permissions = b"drwxr-xr-x" if (typ & 0xF000 == 0x4000) else b"-rw-r--r--"
- d = strings.local_time(date)
+ d = date.local_time(date_)
year,month,day,hour,minute,_,_,_ = d[:8]
if year != now[0] and month != now[1]:
@@ -132,10 +139,10 @@
Module lib.server.ftpservercore
if pattern is None:
accepted = True
else:
- accepted = fnmatch(strings.tostrings(filename), strings.tostrings(pattern))
+ accepted = fnmatch.fnmatch(strings.tostrings(filename), strings.tostrings(pattern))
if accepted:
if quantity > 100:
- date = 0
+ date_ = 0
else:
sta = (0,0,0,0,0,0,0,0,0)
try:
@@ -144,9 +151,9 @@
def close_pasv(self):
""" Close PASV connection """
if self.pasvsocket is not None:
- logger.syslog(b"[FTP] Close PASV")
+ self.log(b"Close PASV")
self.pasvsocket.close()
self.pasvsocket = None
# historically based on :
# https://github.com/jczic/MicroWebSrv/blob/master/microWebSocket.py
# but I have modified a lot, there must still be some original functions.
+# pylint:disable=consider-using-f-string
""" These classes manage http responses and requests.
The set of request and response are in bytes format.
I no longer use strings, because they are between 20 and 30 times slower.
It may sound a bit more complicated, but it's a lot quick.
"""
-
import hashlib
import time
from binascii import hexlify, b2a_base64
@@ -73,7 +73,8 @@
self.content_file = None
self.identifier = None
self.request = request
+ self.chunk_size = 0
def __del__(self):
if self.content_file is not None:
@@ -207,16 +209,19 @@
Module lib.server.httprequest
data = await streamio.readline()
if data != b"":
spl = data.split()
- self.method = spl[0]
- path = spl[1]
- proto = spl[2]
- if self.request is False:
- self.status = path
- paths = path.split(b"?", 1)
- if len(paths) > 1:
- self.unserialize_params(paths[1])
- self.path = self.unquote(paths[0])
- await self.unserialize_headers(streamio)
+ if len(spl) >= 2:
+ self.method = spl[0]
+ path = spl[1]
+ if self.request is False:
+ self.status = path
+ paths = path.split(b"?", 1)
+ if len(paths) > 1:
+ self.unserialize_params(paths[1])
+ self.path = self.unquote(paths[0])
+
+ await self.unserialize_headers(streamio)
+ else:
+ await self.read_content(streamio)
def unserialize_params(self, url):
""" Extract parameters from url """
@@ -226,44 +231,56 @@
Module lib.server.httprequest
param = [self.unquote(x) for x in pair.split(b"=", 1)]
if len(param) == 1:
param.append(True)
- previousValue = self.params.get(param[0])
- if previousValue is not None:
- if previousValue == b'0' and param[1] == b'':
+ previous_value = self.params.get(param[0])
+ if previous_value is not None:
+ if previous_value == b'0' and param[1] == b'':
self.params[param[0]] = b'1'
else:
- if not isinstance(previousValue, list):
- self.params[param[0]] = [previousValue]
+ if not isinstance(previous_value, list):
+ self.params[param[0]] = [previous_value]
self.params[param[0]].append(param[1])
else:
self.params[param[0]] = param[1]
async def read_content(self, streamio):
""" Read the content of http request """
- length = int(self.headers.get(b"Content-Length","0"))
+ if self.headers.get(b"Transfer-Encoding",b"") == b"chunked":
+ length = await streamio.readline()
+ length = eval(strings.tostrings(b"0x%s"%length.strip()))
+ self.chunk_size = length
+ chunk = True
+ else:
+ length = int(self.headers.get(b"Content-Length",b"0"))
+ chunk = False
+ await self.read_data(length, streamio, chunk)
+
+ async def read_data(self, length, streamio, chunk=False):
+ """ Read data with length """
# If data small write in memory
if length < 4096:
- self.content = b""
- while len(self.content) < length:
- self.content += await streamio.read(int(self.headers.get(b"Content-Length","0")))
+ if chunk is False or self.content is None:
+ self.content = b""
+ data = b""
+ while len(data) < length:
+ data += await streamio.read(length - len(data))
+ self.content += data
# Data too big write in file
else:
self.content_file = "%d.tmp"%id(self)
- try:
- content = open(self.content_file, "wb")
+ if chunk is False:
+ attrib = "wb"
+ else:
+ attrib = "ab"
+ with open(self.content_file, attrib) as content:
while content.tell() < length:
- content.write(await streamio.read(int(self.headers.get(b"Content-Length","0"))))
- finally:
- content.close()
+ content.write(await streamio.read(length - len(self.content)))
def get_content_filename(self):
""" Copy the content into file """
if self.content is not None:
self.content_file = "%d.tmp"%id(self)
- try:
- content = open(self.content_file, "wb")
+ with open(self.content_file, "wb") as content:
content.write(self.content)
- finally:
- content.close()
self.content = None
return self.content_file
@@ -337,7 +354,7 @@
Module lib.server.httprequest
async def serialize_body(self, streamio):
""" Serialize body """
result = 0
- noEnd = False
+ no_end = False
# If content existing
if self.content is not None:
try:
@@ -348,8 +365,9 @@
Module lib.server.httprequest
else:
# Serialize object
result += await self.content.serialize(streamio)
- noEnd = True
+ no_end = True
except Exception as err:
+ logger.syslog(err)
# Serialize error detected
result += await streamio.write(b'Content-Type: text/plain\r\n\r\n')
result += await streamio.write(strings.tostrings(logger.exception(err)))
@@ -378,7 +396,7 @@
Module lib.server.httprequest
if self.headers[b"Content-Type"] != b"multipart/x-mixed-replace":
result += await streamio.write(b"--")
- if noEnd is False:
+ if no_end is False:
# Terminate serialize request or response
result += await streamio.write(b"\r\n")
return result
@@ -402,6 +420,7 @@
Module lib.server.httprequest
""" Class that contains a file """
def __init__(self, filename, content_type=None, base64=False):
""" Constructor """
+ # pylint:disable=global-variable-not-assigned
if type(filename) == type([]):
self.filenames = filename
else:
@@ -414,48 +433,60 @@
Module lib.server.httprequest
else:
self.content_type = content_type
+ async def serialize_file(self, filename, streamio):
+ """ Serialize the content of file named filename """
+ result = 0
+ found = False
+
+ filename = strings.tostrings(filename)
+
+ # If file existing
+ if filesystem.exists(filename):
+ with open(strings.tostrings(filename), "rb") as f:
+ found = True
+ result = await streamio.write(b'Content-Type: %s\r\n\r\n'%(self.content_type))
+ if server.stream.Bufferedio.is_enough_memory():
+ step = 1440*10
+ else:
+ step = 512
+ buf = bytearray(step)
+ f.seek(0,2)
+ size = f.tell()
+ f.seek(0)
+
+ if self.base64 and step % 3 != 0:
+ step = (step//3)*3
+
+ length_written = 0
+
+ while size > 0:
+ if size < step:
+ buf = bytearray(size)
+ length = f.readinto(buf)
+ size -= length
+ if self.base64:
+ length_written += await streamio.write(b2a_base64(buf))
+ else:
+ length_written += await streamio.write(buf)
+ result += length_written
+ else:
+ logger.syslog("%s file not found"%filename)
+ return result, found
+
async def serialize(self, streamio):
""" Serialize file """
found = False
+ result = 0
try:
- f = None
# print("Begin send %s"%strings.tostrings(self.filename))
for filename in self.filenames:
- if filesystem.exists(filename):
- f = open(strings.tostrings(filename), "rb")
- if found is False:
- result = await streamio.write(b'Content-Type: %s\r\n\r\n'%(self.content_type))
+ r, f = await self.serialize_file(filename, streamio)
+ result += r
+ if f:
found = True
- if server.stream.Bufferedio.is_enough_memory():
- step = 1440*10
- else:
- step = 512
- buf = bytearray(step)
- f.seek(0,2)
- size = f.tell()
- f.seek(0)
-
- if self.base64 and step % 3 != 0:
- step = (step//3)*3
-
- lengthWritten = 0
-
- while size > 0:
- if size < step:
- buf = bytearray(size)
- length = f.readinto(buf)
- size -= length
- if self.base64:
- lengthWritten += await streamio.write(b2a_base64(buf))
- else:
- lengthWritten += await streamio.write(buf)
- # print("End send %s"%strings.tostrings(self.filename))
- result += lengthWritten
+ # print("End send %s"%strings.tostrings(self.filename))
except Exception as err:
pass
- finally:
- if f:
- f.close()
if found is False:
result = await streamio.write(b'Content-Type: text/plain\r\n\r\n')
filenames = b""
@@ -468,6 +499,7 @@
Module lib.server.httprequest
""" Class that contains a buffer """
def __init__(self, filename, buffer, content_type=None):
""" Constructor """
+ # pylint:disable=global-variable-not-assigned
self.filename = filename
self.buffer = buffer
if content_type is None:
@@ -525,21 +557,17 @@
Module lib.server.httprequest
async def serialize(self, identifier, streamio):
""" Serialize multi part file """
result = await self.serialize_header(identifier, streamio)
- try:
- part = b""
- file = open(strings.tostrings(self.filename),"rb")
+ with open(strings.tostrings(self.filename),"rb") as file:
part = file.read()
- finally:
- file.close()
result += await streamio.write(b"\r\n%s\r\n"%part)
result += await streamio.write(b"--%s"%identifier)
return result
async def get_size(self, identifier):
""" Get the size of multi part file """
- headerSize = await self.serialize_header(identifier, server.stream.Bytesio())
- fileSize = filesystem.filesize((strings.tostrings(self.filename)))
- return headerSize + fileSize + 4 + len(identifier) + 2
+ header_size = await self.serialize_header(identifier, server.stream.Bytesio())
+ file_size = filesystem.filesize((strings.tostrings(self.filename)))
+ return header_size + file_size + 4 + len(identifier) + 2
class PartBin(PartFile):
""" Class that contains a binary data, used in multipart request or response """
@@ -565,6 +593,7 @@
async def serialize_file(self, filename, streamio):
+ """ Serialize the content of file named filename """
+ result = 0
+ found = False
+
+ filename = strings.tostrings(filename)
+
+ # If file existing
+ if filesystem.exists(filename):
+ with open(strings.tostrings(filename), "rb") as f:
+ found = True
+ result = await streamio.write(b'Content-Type: %s\r\n\r\n'%(self.content_type))
+ if server.stream.Bufferedio.is_enough_memory():
+ step = 1440*10
+ else:
+ step = 512
+ buf = bytearray(step)
+ f.seek(0,2)
+ size = f.tell()
+ f.seek(0)
+
+ if self.base64 and step % 3 != 0:
+ step = (step//3)*3
+
+ length_written = 0
+
+ while size > 0:
+ if size < step:
+ buf = bytearray(size)
+ length = f.readinto(buf)
+ size -= length
+ if self.base64:
+ length_written += await streamio.write(b2a_base64(buf))
+ else:
+ length_written += await streamio.write(buf)
+ result += length_written
+ else:
+ logger.syslog("%s file not found"%filename)
+ return result, found
+
+
@@ -911,6 +986,7 @@
Methods
self.content_file = None
self.identifier = None
self.request = request
+ self.chunk_size = 0
def __del__(self):
if self.content_file is not None:
@@ -1024,16 +1100,19 @@
Methods
data = await streamio.readline()
if data != b"":
spl = data.split()
- self.method = spl[0]
- path = spl[1]
- proto = spl[2]
- if self.request is False:
- self.status = path
- paths = path.split(b"?", 1)
- if len(paths) > 1:
- self.unserialize_params(paths[1])
- self.path = self.unquote(paths[0])
- await self.unserialize_headers(streamio)
+ if len(spl) >= 2:
+ self.method = spl[0]
+ path = spl[1]
+ if self.request is False:
+ self.status = path
+ paths = path.split(b"?", 1)
+ if len(paths) > 1:
+ self.unserialize_params(paths[1])
+ self.path = self.unquote(paths[0])
+
+ await self.unserialize_headers(streamio)
+ else:
+ await self.read_content(streamio)
def unserialize_params(self, url):
""" Extract parameters from url """
@@ -1043,44 +1122,56 @@
Methods
param = [self.unquote(x) for x in pair.split(b"=", 1)]
if len(param) == 1:
param.append(True)
- previousValue = self.params.get(param[0])
- if previousValue is not None:
- if previousValue == b'0' and param[1] == b'':
+ previous_value = self.params.get(param[0])
+ if previous_value is not None:
+ if previous_value == b'0' and param[1] == b'':
self.params[param[0]] = b'1'
else:
- if not isinstance(previousValue, list):
- self.params[param[0]] = [previousValue]
+ if not isinstance(previous_value, list):
+ self.params[param[0]] = [previous_value]
self.params[param[0]].append(param[1])
else:
self.params[param[0]] = param[1]
async def read_content(self, streamio):
""" Read the content of http request """
- length = int(self.headers.get(b"Content-Length","0"))
+ if self.headers.get(b"Transfer-Encoding",b"") == b"chunked":
+ length = await streamio.readline()
+ length = eval(strings.tostrings(b"0x%s"%length.strip()))
+ self.chunk_size = length
+ chunk = True
+ else:
+ length = int(self.headers.get(b"Content-Length",b"0"))
+ chunk = False
+ await self.read_data(length, streamio, chunk)
+
+ async def read_data(self, length, streamio, chunk=False):
+ """ Read data with length """
# If data small write in memory
if length < 4096:
- self.content = b""
- while len(self.content) < length:
- self.content += await streamio.read(int(self.headers.get(b"Content-Length","0")))
+ if chunk is False or self.content is None:
+ self.content = b""
+ data = b""
+ while len(data) < length:
+ data += await streamio.read(length - len(data))
+ self.content += data
# Data too big write in file
else:
self.content_file = "%d.tmp"%id(self)
- try:
- content = open(self.content_file, "wb")
+ if chunk is False:
+ attrib = "wb"
+ else:
+ attrib = "ab"
+ with open(self.content_file, attrib) as content:
while content.tell() < length:
- content.write(await streamio.read(int(self.headers.get(b"Content-Length","0"))))
- finally:
- content.close()
+ content.write(await streamio.read(length - len(self.content)))
def get_content_filename(self):
""" Copy the content into file """
if self.content is not None:
self.content_file = "%d.tmp"%id(self)
- try:
- content = open(self.content_file, "wb")
+ with open(self.content_file, "wb") as content:
content.write(self.content)
- finally:
- content.close()
self.content = None
return self.content_file
@@ -1154,7 +1245,7 @@
Methods
async def serialize_body(self, streamio):
""" Serialize body """
result = 0
- noEnd = False
+ no_end = False
# If content existing
if self.content is not None:
try:
@@ -1165,8 +1256,9 @@
Methods
else:
# Serialize object
result += await self.content.serialize(streamio)
- noEnd = True
+ no_end = True
except Exception as err:
+ logger.syslog(err)
# Serialize error detected
result += await streamio.write(b'Content-Type: text/plain\r\n\r\n')
result += await streamio.write(strings.tostrings(logger.exception(err)))
@@ -1195,7 +1287,7 @@
Methods
if self.headers[b"Content-Type"] != b"multipart/x-mixed-replace":
result += await streamio.write(b"--")
- if noEnd is False:
+ if no_end is False:
# Terminate serialize request or response
result += await streamio.write(b"\r\n")
return result
@@ -1248,11 +1340,8 @@
Methods
""" Copy the content into file """
if self.content is not None:
self.content_file = "%d.tmp"%id(self)
- try:
- content = open(self.content_file, "wb")
+ with open(self.content_file, "wb") as content:
content.write(self.content)
- finally:
- content.close()
self.content = None
return self.content_file
@@ -1384,21 +1473,46 @@
async def read_data(self, length, streamio, chunk=False):
+ """ Read data with length """
# If data small write in memory
if length < 4096:
- self.content = b""
- while len(self.content) < length:
- self.content += await streamio.read(int(self.headers.get(b"Content-Length","0")))
+ if chunk is False or self.content is None:
+ self.content = b""
+ data = b""
+ while len(data) < length:
+ data += await streamio.read(length - len(data))
+ self.content += data
# Data too big write in file
else:
self.content_file = "%d.tmp"%id(self)
- try:
- content = open(self.content_file, "wb")
+ if chunk is False:
+ attrib = "wb"
+ else:
+ attrib = "ab"
+ with open(self.content_file, attrib) as content:
while content.tell() < length:
- content.write(await streamio.read(int(self.headers.get(b"Content-Length","0"))))
- finally:
- content.close()
async def serialize_body(self, streamio):
""" Serialize body """
result = 0
- noEnd = False
+ no_end = False
# If content existing
if self.content is not None:
try:
@@ -1444,8 +1558,9 @@
Methods
else:
# Serialize object
result += await self.content.serialize(streamio)
- noEnd = True
+ no_end = True
except Exception as err:
+ logger.syslog(err)
# Serialize error detected
result += await streamio.write(b'Content-Type: text/plain\r\n\r\n')
result += await streamio.write(strings.tostrings(logger.exception(err)))
@@ -1474,7 +1589,7 @@
Methods
if self.headers[b"Content-Type"] != b"multipart/x-mixed-replace":
result += await streamio.write(b"--")
- if noEnd is False:
+ if no_end is False:
# Terminate serialize request or response
result += await streamio.write(b"\r\n")
return result
@@ -1660,16 +1775,19 @@
Methods
data = await streamio.readline()
if data != b"":
spl = data.split()
- self.method = spl[0]
- path = spl[1]
- proto = spl[2]
- if self.request is False:
- self.status = path
- paths = path.split(b"?", 1)
- if len(paths) > 1:
- self.unserialize_params(paths[1])
- self.path = self.unquote(paths[0])
- await self.unserialize_headers(streamio)
+ if len(spl) >= 2:
+ self.method = spl[0]
+ path = spl[1]
+ if self.request is False:
+ self.status = path
+ paths = path.split(b"?", 1)
+ if len(paths) > 1:
+ self.unserialize_params(paths[1])
+ self.path = self.unquote(paths[0])
+
+ await self.unserialize_headers(streamio)
+ else:
+ await self.read_content(streamio)
@@ -1719,13 +1837,13 @@
Methods
param = [self.unquote(x) for x in pair.split(b"=", 1)]
if len(param) == 1:
param.append(True)
- previousValue = self.params.get(param[0])
- if previousValue is not None:
- if previousValue == b'0' and param[1] == b'':
+ previous_value = self.params.get(param[0])
+ if previous_value is not None:
+ if previous_value == b'0' and param[1] == b'':
self.params[param[0]] = b'1'
else:
- if not isinstance(previousValue, list):
- self.params[param[0]] = [previousValue]
+ if not isinstance(previous_value, list):
+ self.params[param[0]] = [previous_value]
self.params[param[0]].append(param[1])
else:
self.params[param[0]] = param[1]
@@ -1757,7 +1875,7 @@
Methods
streamio = self.streamio
await self.unserialize(streamio)
- async def send(self, streamio):
+ async def send(self, streamio=None):
""" Send request to server """
if streamio is None:
streamio = self.streamio
@@ -1786,7 +1904,7 @@
async def serialize(self, identifier, streamio):
""" Serialize multi part file """
result = await self.serialize_header(identifier, streamio)
- try:
- part = b""
- file = open(strings.tostrings(self.filename),"rb")
+ with open(strings.tostrings(self.filename),"rb") as file:
part = file.read()
- finally:
- file.close()
result += await streamio.write(b"\r\n%s\r\n"%part)
result += await streamio.write(b"--%s"%identifier)
return result
# historically based on :
# https://github.com/jczic/MicroWebSrv/blob/master/microWebSocket.py
# but I have modified a lot, there must still be some original functions.
+# pylint:disable=consider-using-f-string
""" This class is used to manage an http server.
This class contains few lines of code, this is to save memory.
The core of the server is in the other class HttpServerCore, which is loaded into memory only when connecting an HTTP client.
@@ -74,7 +75,7 @@
Module lib.server.httpserver
try:
loader()
except ModuleNotFound as err:
- logger.syslog("Failed to preload html page, %s"%str(err))
+ logger.syslog("Preload html page : %s"%str(err))
except Exception as err:
logger.syslog(err)
@@ -301,7 +302,7 @@
Classes
try:
loader()
except ModuleNotFound as err:
- logger.syslog("Failed to preload html page, %s"%str(err))
+ logger.syslog("Preload html page : %s"%str(err))
except Exception as err:
logger.syslog(err)
@@ -598,7 +599,7 @@
Methods
try:
loader()
except ModuleNotFound as err:
- logger.syslog("Failed to preload html page, %s"%str(err))
+ logger.syslog("Preload html page : %s"%str(err))
except Exception as err:
logger.syslog(err)
diff --git a/doc/lib/server/httpservercore.html b/doc/lib/server/httpservercore.html
index 22e3e9d..d2c3a8a 100644
--- a/doc/lib/server/httpservercore.html
+++ b/doc/lib/server/httpservercore.html
@@ -38,7 +38,7 @@
Module lib.server.httpservercore
from server.httpserver import HttpServer
from server.httprequest import HttpRequest, HttpResponse
from server.stream import Stream
-from tools import logger,strings
+from tools import logger
class HttpServerCore:
""" Http server core, it instanciated only if a connection is done to the asynchronous class HttpServer then
@@ -59,7 +59,7 @@
Module lib.server.httpservercore
# print(request.path)
function, args = HttpServer.search_route(request)
if function is None:
- await response.send_error(status=b"404", content=b"Page not found")
+ await response.send_not_found()
else:
await function(request, response, args)
except OSError as err:
@@ -68,9 +68,10 @@
if login is not None:
from server.notifier import Notifier
if login:
- await Notifier.notify(lang.login_success_detected, display=False, enabled=self.server_config.notify)
+ if self.last_success_notification is None:
+ notif = True
+ self.last_success_notification = self.current_time
+ elif self.last_success_notification + 5*60 < self.current_time:
+ self.last_success_notification = self.current_time
+ notif = True
+ else:
+ notif = False
+ if notif:
+ await Notifier.notify(lang.login_success_detected, display=False, enabled=self.server_config.notify)
else:
await Notifier.notify(lang.login_failed_detected, display=False, enabled=self.server_config.notify)
async def task(self):
""" Periodic task method """
+ if support.battery():
+ from tools import battery
+
polling_id = 0
watchdog.WatchDog.start(watchdog.SHORT_WATCH_DOG)
@@ -89,12 +104,14 @@
Module lib.server.periodic
# Reset brownout counter if wifi connected
if wifi.Wifi.is_wan_connected():
- battery.Battery.reset_brownout()
+ if support.battery():
+ battery.Battery.reset_brownout()
# Reset watch dog
watchdog.WatchDog.feed()
await uasyncio.sleep(1)
polling_id += 1
+ self.current_time += 1
# Check if any problems have occurred and if a reboot is needed
if polling_id % 3607:
@@ -146,6 +163,8 @@
if login is not None:
from server.notifier import Notifier
if login:
- await Notifier.notify(lang.login_success_detected, display=False, enabled=self.server_config.notify)
+ if self.last_success_notification is None:
+ notif = True
+ self.last_success_notification = self.current_time
+ elif self.last_success_notification + 5*60 < self.current_time:
+ self.last_success_notification = self.current_time
+ notif = True
+ else:
+ notif = False
+ if notif:
+ await Notifier.notify(lang.login_success_detected, display=False, enabled=self.server_config.notify)
else:
await Notifier.notify(lang.login_failed_detected, display=False, enabled=self.server_config.notify)
async def task(self):
""" Periodic task method """
+ if support.battery():
+ from tools import battery
+
polling_id = 0
watchdog.WatchDog.start(watchdog.SHORT_WATCH_DOG)
@@ -188,12 +219,14 @@
Classes
# Reset brownout counter if wifi connected
if wifi.Wifi.is_wan_connected():
- battery.Battery.reset_brownout()
+ if support.battery():
+ battery.Battery.reset_brownout()
# Reset watch dog
watchdog.WatchDog.feed()
await uasyncio.sleep(1)
polling_id += 1
+ self.current_time += 1
# Check if any problems have occurred and if a reboot is needed
if polling_id % 3607:
@@ -225,7 +258,16 @@
Methods
if login is not None:
from server.notifier import Notifier
if login:
- await Notifier.notify(lang.login_success_detected, display=False, enabled=self.server_config.notify)
+ if self.last_success_notification is None:
+ notif = True
+ self.last_success_notification = self.current_time
+ elif self.last_success_notification + 5*60 < self.current_time:
+ self.last_success_notification = self.current_time
+ notif = True
+ else:
+ notif = False
+ if notif:
+ await Notifier.notify(lang.login_success_detected, display=False, enabled=self.server_config.notify)
else:
await Notifier.notify(lang.login_failed_detected, display=False, enabled=self.server_config.notify)
# Reset brownout counter if wifi connected
if wifi.Wifi.is_wan_connected():
- battery.Battery.reset_brownout()
+ if support.battery():
+ battery.Battery.reset_brownout()
# Reset watch dog
watchdog.WatchDog.feed()
await uasyncio.sleep(1)
polling_id += 1
+ self.current_time += 1
# Check if any problems have occurred and if a reboot is needed
if polling_id % 3607:
diff --git a/doc/lib/server/ping.html b/doc/lib/server/ping.html
index 4567d44..091bfac 100644
--- a/doc/lib/server/ping.html
+++ b/doc/lib/server/ping.html
@@ -38,6 +38,7 @@
Module lib.server.ping
# Author: Olav Morken
# https://github.com/olavmrk/python-ping/blob/master/ping.py
# @data: bytes
+# pylint:disable=consider-using-f-string
""" Ping network class """
import time
import random
diff --git a/doc/lib/server/presence.html b/doc/lib/server/presence.html
index 193cf7f..9fd1e17 100644
--- a/doc/lib/server/presence.html
+++ b/doc/lib/server/presence.html
@@ -29,6 +29,7 @@
Module lib.server.presence
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Presence detection (determine if an occupant is present in the house) """
import time
import wifi
diff --git a/doc/lib/server/pushover.html b/doc/lib/server/pushover.html
index 2a3f318..96f43f5 100644
--- a/doc/lib/server/pushover.html
+++ b/doc/lib/server/pushover.html
@@ -79,7 +79,6 @@
preload : True force the load of page at the start,
False the load of page is done a the first http connection (Takes time on first connection) """
Server.context = ServerContext(loop, page_loader, preload, http_port)
- logger.syslog(info.sysinfo(display=False))
+ logger.syslog(info.sysinfo())
logger.syslog("Version: %s"%strings.tostrings(builddate.date))
from server.periodic import periodic_task
@@ -183,11 +203,12 @@
@staticmethod
def is_one_per_day():
""" Indicates if the action must be done on per day """
- date = strings.date_to_bytes()[:14]
- if Server.context.one_per_day is None or (date[-2:] == b"12" and date != Server.context.one_per_day):
- Server.context.one_per_day = date
+ date_ = date.date_to_bytes()[:14]
+ if Server.context.one_per_day is None or (date_[-2:] == b"12" and date_ != Server.context.one_per_day):
+ Server.context.one_per_day = date_
return True
return False
@@ -256,9 +277,10 @@
Module lib.server.server
# If telnet activated
if Server.context.server_config.telnet:
- # Load and start telnet
- import server.telnet
- server.telnet.start()
+ if support.telnet():
+ # Load and start telnet
+ import server.telnet
+ server.telnet.start()
# If ftp activated
if Server.context.server_config.ftp:
@@ -364,6 +386,22 @@
preload : True force the load of page at the start,
False the load of page is done a the first http connection (Takes time on first connection) """
Server.context = ServerContext(loop, page_loader, preload, http_port)
- logger.syslog(info.sysinfo(display=False))
+ logger.syslog(info.sysinfo())
logger.syslog("Version: %s"%strings.tostrings(builddate.date))
from server.periodic import periodic_task
@@ -465,11 +503,12 @@
@staticmethod
def is_one_per_day():
""" Indicates if the action must be done on per day """
- date = strings.date_to_bytes()[:14]
- if Server.context.one_per_day is None or (date[-2:] == b"12" and date != Server.context.one_per_day):
- Server.context.one_per_day = date
+ date_ = date.date_to_bytes()[:14]
+ if Server.context.one_per_day is None or (date_[-2:] == b"12" and date_ != Server.context.one_per_day):
+ Server.context.one_per_day = date_
return True
return False
@@ -538,9 +577,10 @@
Classes
# If telnet activated
if Server.context.server_config.telnet:
- # Load and start telnet
- import server.telnet
- server.telnet.start()
+ if support.telnet():
+ # Load and start telnet
+ import server.telnet
+ server.telnet.start()
# If ftp activated
if Server.context.server_config.ftp:
@@ -627,6 +667,10 @@
preload : True force the load of page at the start,
False the load of page is done a the first http connection (Takes time on first connection) """
Server.context = ServerContext(loop, page_loader, preload, http_port)
- logger.syslog(info.sysinfo(display=False))
+ logger.syslog(info.sysinfo())
logger.syslog("Version: %s"%strings.tostrings(builddate.date))
from server.periodic import periodic_task
@@ -701,9 +765,9 @@
Static methods
@staticmethod
def is_one_per_day():
""" Indicates if the action must be done on per day """
- date = strings.date_to_bytes()[:14]
- if Server.context.one_per_day is None or (date[-2:] == b"12" and date != Server.context.one_per_day):
- Server.context.one_per_day = date
+ date_ = date.date_to_bytes()[:14]
+ if Server.context.one_per_day is None or (date_[-2:] == b"12" and date_ != Server.context.one_per_day):
+ Server.context.one_per_day = date_
return True
return False
@@ -812,6 +876,21 @@
Static methods
Server.suspended[0] = False
+
+def set_daily_notifier(callback)
+
+
+
Replace the daily notification (callback which return a string with message to notify)
+
+
+Expand source code
+
+
@staticmethod
+def set_daily_notifier(callback):
+ """ Replace the daily notification (callback which return a string with message to notify) """
+ Server.daily_notifier = callback
+
+
def slow_down(duration=20)
@@ -852,9 +931,10 @@
Static methods
# If telnet activated
if Server.context.server_config.telnet:
- # Load and start telnet
- import server.telnet
- server.telnet.start()
+ if support.telnet():
+ # Load and start telnet
+ import server.telnet
+ server.telnet.start()
# If ftp activated
if Server.context.server_config.ftp:
@@ -981,11 +1061,12 @@
Static methods
wifi.Wifi.wan_disconnected()
if forced:
- await Server.context.notifier.notify("\n - Lan Ip : %s\n - Wan Ip : %s\n - Uptime : %s\n - %s"%(
- wifi.Station.get_info()[0],
- Server.context.wan_ip,
- info.uptime(),
- strings.tostrings(info.flashinfo(mountpoint=sdcard.SdCard.get_mountpoint(), display=False))))
+ try:
+ # pylint:disable=not-callable
+ message = Server.daily_notifier()
+ except:
+ message = Server.default_daily_notifier()
+ await Server.context.notifier.notify(message)
""" Class used to store http connection sessions, it is useful if you define
an user and password, on your site """
import time
-from tools import encryption, strings
+from tools import encryption, strings, date
class Sessions:
""" Class manage an http sessions """
@@ -43,7 +43,7 @@
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Function which sets the internal clock of the card based on an ntp server """
-import time
-from tools import logger,strings,filesystem
+from tools import logger, strings, date
def get_ntp_time():
""" Return the time from a NTP server """
@@ -58,7 +58,7 @@
Module lib.server.timesetting
def set_time(currenttime):
""" Change the current time """
try:
- newtime = strings.local_time(currenttime)
+ newtime = date.local_time(currenttime)
year,month,day,hour,minute,second,weekday,yearday = newtime[:8]
import machine
@@ -66,31 +66,22 @@
Module lib.server.timesetting
except Exception as exc:
logger.syslog("Cannot set time '%s'"%exc)
-def mktime(t):
- """ Portable mktime """
- year,month,day,hour,minute,second,weekday,yearday = t
- if filesystem.ismicropython():
- return time.mktime((year, month, day, hour, minute, second, weekday, yearday))
- else:
- return time.mktime((year, month, day, hour, minute, second, weekday, yearday, 0))
-
-
def calc_local_time(currenttime, offsetTime=+1, dst=True):
""" Calculate the local time """
- year,month,day,hour,minute,second,weekday,yearday = strings.local_time(currenttime)[:8]
+ year,month,day,hour,minute,second,weekday,yearday = date.local_time(currenttime)[:8]
# Get the day of the last sunday of march
- march_end_weekday = strings.local_time(mktime((year, 3, 31, 0, 0, 0, 0, 0)))[6]
+ march_end_weekday = date.local_time(date.mktime((year, 3, 31, 0, 0, 0, 0, 0)))[6]
start_day_dst = 31-((1+march_end_weekday)%7)
# Get the day of the last sunday of october
- october_end_weekday = strings.local_time(mktime((year,10, 30, 0, 0, 0, 0, 0)))[6]
+ october_end_weekday = date.local_time(date.mktime((year,10, 30, 0, 0, 0, 0, 0)))[6]
end_day_dst = 30-((1+october_end_weekday)%7)
- start_DST = mktime((year,3 ,start_day_dst,1,0,0,0,0))
- end_DST = mktime((year,10,end_day_dst ,1,0,0,0,0))
+ start_DST = date.mktime((year,3 ,start_day_dst,1,0,0,0,0))
+ end_DST = date.mktime((year,10,end_day_dst ,1,0,0,0,0))
- now = mktime((year,month,day,hour,minute,second,weekday,yearday))
+ now = date.mktime((year,month,day,hour,minute,second,weekday,yearday))
if dst and now > start_DST and now < end_DST : # we are before last sunday of october
return now+(offsetTime*3600)+3600 # DST: UTC+dst*H + 1
@@ -105,7 +96,7 @@
def calc_local_time(currenttime, offsetTime=+1, dst=True):
""" Calculate the local time """
- year,month,day,hour,minute,second,weekday,yearday = strings.local_time(currenttime)[:8]
+ year,month,day,hour,minute,second,weekday,yearday = date.local_time(currenttime)[:8]
# Get the day of the last sunday of march
- march_end_weekday = strings.local_time(mktime((year, 3, 31, 0, 0, 0, 0, 0)))[6]
+ march_end_weekday = date.local_time(date.mktime((year, 3, 31, 0, 0, 0, 0, 0)))[6]
start_day_dst = 31-((1+march_end_weekday)%7)
# Get the day of the last sunday of october
- october_end_weekday = strings.local_time(mktime((year,10, 30, 0, 0, 0, 0, 0)))[6]
+ october_end_weekday = date.local_time(date.mktime((year,10, 30, 0, 0, 0, 0, 0)))[6]
end_day_dst = 30-((1+october_end_weekday)%7)
- start_DST = mktime((year,3 ,start_day_dst,1,0,0,0,0))
- end_DST = mktime((year,10,end_day_dst ,1,0,0,0,0))
+ start_DST = date.mktime((year,3 ,start_day_dst,1,0,0,0,0))
+ end_DST = date.mktime((year,10,end_day_dst ,1,0,0,0,0))
- now = mktime((year,month,day,hour,minute,second,weekday,yearday))
+ now = date.mktime((year,month,day,hour,minute,second,weekday,yearday))
if dst and now > start_DST and now < end_DST : # we are before last sunday of october
return now+(offsetTime*3600)+3600 # DST: UTC+dst*H + 1
@@ -181,24 +172,6 @@
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Class used to manage a username and a password """
from tools import logger,jsonconfig,encryption,strings,info
diff --git a/doc/lib/server/wanip.html b/doc/lib/server/wanip.html
index 00ff0b3..bbe8655 100644
--- a/doc/lib/server/wanip.html
+++ b/doc/lib/server/wanip.html
@@ -50,7 +50,6 @@
Module lib.server.wanip
req.set_header(b"Accept" ,b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.set_header(b"User-Agent" ,b"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15")
req.set_header(b"Accept-Language",b"fr-FR,fr;q=0.9")
- req.set_header(b"Accept-Encoding",b"gzip, deflate")
req.set_header(b"Connection" ,b"keep-alive")
await req.send(streamio)
response = HttpResponse(streamio)
@@ -138,7 +137,6 @@
Functions
req.set_header(b"Accept" ,b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.set_header(b"User-Agent" ,b"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Safari/605.1.15")
req.set_header(b"Accept-Language",b"fr-FR,fr;q=0.9")
- req.set_header(b"Accept-Encoding",b"gzip, deflate")
req.set_header(b"Connection" ,b"keep-alive")
await req.send(streamio)
response = HttpResponse(streamio)
diff --git a/doc/lib/shell/editor.html b/doc/lib/shell/editor.html
index 3903112..747636d 100644
--- a/doc/lib/shell/editor.html
+++ b/doc/lib/shell/editor.html
@@ -40,6 +40,9 @@
Module lib.shell.editor
# Copyright (c) 2021 Remi BERTHOLET
# pylint:disable=multiple-statements
# pylint:disable=too-many-lines
+# pylint:disable=consider-using-f-string
+# pylint:disable=unspecified-encoding
+
""" Class defining a VT100 text editor.
This editor works directly in the board.
This allows you to make quick and easy changes directly on the board, without having to use synchronization tools.
@@ -374,6 +377,8 @@
logger.syslog(err)
self.lines = [""]
- def save(self):
+ def save(self, filename=None):
""" Save text in the file """
result = False
if self.read_only is False:
- if self.filename is not None:
+ if filename is None:
+ filename = self.filename
+ if filename is not None:
try:
- file = open(self.filename, "w")
+ file = open(filename, "w")
for line in self.lines:
file.write(line)
file.close()
@@ -989,7 +1000,7 @@
Module lib.shell.editor
def get_selection(self):
""" Get information about selection """
- if self.selection_start:
+ if self.selection_start is not None and self.selection_end is not None:
if self.selection_start[1] > self.selection_end[1]:
return self.selection_end, self.selection_start
elif self.selection_start[1] < self.selection_end[1]:
@@ -1375,7 +1386,7 @@
Module lib.shell.editor
def change_case(self, keys=None):
""" Change the case of selection """
selection = self.copy_clipboard()
- if selection != []:
+ if len(selection) > 0:
self.modified = True
selection_start = self.selection_start
selection_end = self.selection_end
@@ -1660,21 +1671,19 @@
Module lib.shell.editor
if len(keys[0]) > 3:
# If camflasher mouse selection
if keys[0][0:2] == "\x1B[" and keys[0][-1] in ["x","y"]:
- if keys[0][-1] == "y":
- end = True
- else:
-
+ try:
+ pos = keys[0][2:-1]
+ line, column = pos.split(";")
self.begin_line, self.begin_column = self.view.get_position()
- self.hide_selection()
- end = False
- pos = keys[0][2:-1]
- line, column = pos.split(";")
-
- if end:
- self.open_selection()
- self.goto(int(line)+self.begin_line,int(column)+self.begin_column, not end)
- if end:
- self.close_selection()
+ if keys[0][-1] == "x":
+ self.goto(int(line)+self.begin_line,int(column)+self.begin_column, True)
+ self.open_selection()
+ self.close_selection()
+ else:
+ self.goto(int(line)+self.begin_line,int(column)+self.begin_column, False)
+ self.close_selection()
+ except Exception as err:
+ pass
class Edit:
""" Class which aggregate the View and Text """
@@ -1687,20 +1696,19 @@
def run(self):
""" Core of the editor """
- self.edit.view.cls()
- self.edit.view.get_screen_size()
- self.loop = True
- self.EDIT_KEYS = self.cfg.key_toggle_mode+self.cfg.key_find+self.cfg.key_replace+self.cfg.key_find_previous+self.cfg.key_find_next+self.cfg.key_exit+self.cfg.key_goto+self.cfg.key_save+self.cfg.key_execute
- while(self.loop):
- try:
- self.refresh()
- keys = self.get_key(duration=0.4)
- if len(keys[0]) == 0:
- self.is_refresh_header = True
- self.refresh_header()
- keys = self.get_key()
-
- if keys == ["\x1B[23~"]:
- keys = ["\x1B[1;5x"]
- if keys == ["\x1B[24~"]:
- keys = ["\x1B[5;1y"]
-
- if self.trace is not None:
- for key in keys:
- self.trace.write(strings.dump(key, withColor=False) + "\n")
- self.trace.flush()
- modified = self.edit.text.modified
- self.precedent_callback = self.key_callback
- self.key_callback = None
-
- if ord(keys[0][0]) < 0x20:
- if keys[0] in self.EDIT_KEYS:
- if keys[0] in self.cfg.key_toggle_mode: self.key_callback = self.toggle_mode
- elif keys[0] in self.cfg.key_find: self.key_callback = self.find
- elif keys[0] in self.cfg.key_replace: self.key_callback = self.replace
- elif keys[0] in self.cfg.key_find_previous: self.key_callback = self.find_previous
- elif keys[0] in self.cfg.key_find_next: self.key_callback = self.find_next
- elif keys[0] in self.cfg.key_exit: self.key_callback = self.exit
- elif keys[0] in self.cfg.key_goto: self.key_callback = self.goto
- elif keys[0] in self.cfg.key_save: self.key_callback = self.save
- elif keys[0] in self.cfg.key_execute: self.key_callback = self.execute
-
- # If a replacement is in progress and new line pressed
- if keys[0] in self.cfg.key_new_line:
- if self.precedent_callback is not None:
- # The next check is compatible with micropython
- if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
- if self.replace_text is not None:
- # Replace current found
- self.key_callback = self.replace_current
- # If a replacement is in progress and select all pressed
- if keys[0] in self.cfg.key_sel_all:
- if self.precedent_callback is not None:
- # The next check is compatible with micropython
- if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
- if self.replace_text is not None:
- # Replace all
- self.key_callback = self.replace_all
-
- if self.key_callback is not None:
- self.key_callback(keys)
- else:
- self.edit.text.treat_key(keys)
- if modified != self.edit.text.modified:
- self.is_refresh_header = True
- except KeyboardInterrupt:
- pass
- self.edit.view.reset_scroll_region()
- self.edit.view.reset()
+ try:
+ self.edit.view.cls()
+ self.edit.view.get_screen_size()
+ self.loop = True
+ self.EDIT_KEYS = self.cfg.key_toggle_mode+self.cfg.key_find+self.cfg.key_replace+self.cfg.key_find_previous+self.cfg.key_find_next+self.cfg.key_exit+self.cfg.key_goto+self.cfg.key_save+self.cfg.key_execute
+ while(self.loop):
+ try:
+ self.refresh()
+ keys = self.get_key(duration=0.4)
+ if len(keys[0]) == 0:
+ self.is_refresh_header = True
+ self.refresh_header()
+ keys = self.get_key()
+
+ if keys == ["\x1B[23~"]:
+ keys = ["\x1B[15;5x"]
+ if keys == ["\x1B[24~"]:
+ keys = ["\x1B[20;5y"]
+
+ if self.trace is not None:
+ for key in keys:
+ self.trace.write(strings.dump(key, withColor=False) + "\n")
+ self.trace.flush()
+ modified = self.edit.text.modified
+ self.precedent_callback = self.key_callback
+ self.key_callback = None
+
+ if ord(keys[0][0]) < 0x20:
+ if keys[0] in self.EDIT_KEYS:
+ if keys[0] in self.cfg.key_toggle_mode: self.key_callback = self.toggle_mode
+ elif keys[0] in self.cfg.key_find: self.key_callback = self.find
+ elif keys[0] in self.cfg.key_replace: self.key_callback = self.replace
+ elif keys[0] in self.cfg.key_find_previous: self.key_callback = self.find_previous
+ elif keys[0] in self.cfg.key_find_next: self.key_callback = self.find_next
+ elif keys[0] in self.cfg.key_exit: self.key_callback = self.exit
+ elif keys[0] in self.cfg.key_goto: self.key_callback = self.goto
+ elif keys[0] in self.cfg.key_save: self.key_callback = self.save
+ elif keys[0] in self.cfg.key_execute: self.key_callback = self.execute
+
+ # If a replacement is in progress and new line pressed
+ if keys[0] in self.cfg.key_new_line:
+ if self.precedent_callback is not None:
+ # The next check is compatible with micropython
+ if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
+ if self.replace_text is not None:
+ # Replace current found
+ self.key_callback = self.replace_current
+ # If a replacement is in progress and select all pressed
+ if keys[0] in self.cfg.key_sel_all:
+ if self.precedent_callback is not None:
+ # The next check is compatible with micropython
+ if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
+ if self.replace_text is not None:
+ # Replace all
+ self.key_callback = self.replace_all
+
+ if self.key_callback is not None:
+ self.key_callback(keys)
+ else:
+ self.edit.text.treat_key(keys)
+ if modified != self.edit.text.modified:
+ self.is_refresh_header = True
+ except KeyboardInterrupt:
+ pass
+ self.edit.view.reset_scroll_region()
+ self.edit.view.reset()
+ except Exception as err:
+ print(logger.exception(err))
+ filename = self.edit.text.getFilename() + "_backup"
+ self.save(filename=filename)
+ print("After the crash, a copy of the file was saved in '%s'"%filename)
-if __name__ == "__main__":
+def main():
+ """ Main function """
if len(sys.argv) > 1:
filename = sys.argv[1]
else:
filename = "newfile.py"
- edit = Editor(filename, read_only=False)
def run(self):
""" Core of the editor """
- self.edit.view.cls()
- self.edit.view.get_screen_size()
- self.loop = True
- self.EDIT_KEYS = self.cfg.key_toggle_mode+self.cfg.key_find+self.cfg.key_replace+self.cfg.key_find_previous+self.cfg.key_find_next+self.cfg.key_exit+self.cfg.key_goto+self.cfg.key_save+self.cfg.key_execute
- while(self.loop):
- try:
- self.refresh()
- keys = self.get_key(duration=0.4)
- if len(keys[0]) == 0:
- self.is_refresh_header = True
- self.refresh_header()
- keys = self.get_key()
-
- if keys == ["\x1B[23~"]:
- keys = ["\x1B[1;5x"]
- if keys == ["\x1B[24~"]:
- keys = ["\x1B[5;1y"]
-
- if self.trace is not None:
- for key in keys:
- self.trace.write(strings.dump(key, withColor=False) + "\n")
- self.trace.flush()
- modified = self.edit.text.modified
- self.precedent_callback = self.key_callback
- self.key_callback = None
-
- if ord(keys[0][0]) < 0x20:
- if keys[0] in self.EDIT_KEYS:
- if keys[0] in self.cfg.key_toggle_mode: self.key_callback = self.toggle_mode
- elif keys[0] in self.cfg.key_find: self.key_callback = self.find
- elif keys[0] in self.cfg.key_replace: self.key_callback = self.replace
- elif keys[0] in self.cfg.key_find_previous: self.key_callback = self.find_previous
- elif keys[0] in self.cfg.key_find_next: self.key_callback = self.find_next
- elif keys[0] in self.cfg.key_exit: self.key_callback = self.exit
- elif keys[0] in self.cfg.key_goto: self.key_callback = self.goto
- elif keys[0] in self.cfg.key_save: self.key_callback = self.save
- elif keys[0] in self.cfg.key_execute: self.key_callback = self.execute
-
- # If a replacement is in progress and new line pressed
- if keys[0] in self.cfg.key_new_line:
- if self.precedent_callback is not None:
- # The next check is compatible with micropython
- if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
- if self.replace_text is not None:
- # Replace current found
- self.key_callback = self.replace_current
- # If a replacement is in progress and select all pressed
- if keys[0] in self.cfg.key_sel_all:
- if self.precedent_callback is not None:
- # The next check is compatible with micropython
- if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
- if self.replace_text is not None:
- # Replace all
- self.key_callback = self.replace_all
-
- if self.key_callback is not None:
- self.key_callback(keys)
- else:
- self.edit.text.treat_key(keys)
- if modified != self.edit.text.modified:
- self.is_refresh_header = True
- except KeyboardInterrupt:
- pass
- self.edit.view.reset_scroll_region()
- self.edit.view.reset()
+ try:
+ self.edit.view.cls()
+ self.edit.view.get_screen_size()
+ self.loop = True
+ self.EDIT_KEYS = self.cfg.key_toggle_mode+self.cfg.key_find+self.cfg.key_replace+self.cfg.key_find_previous+self.cfg.key_find_next+self.cfg.key_exit+self.cfg.key_goto+self.cfg.key_save+self.cfg.key_execute
+ while(self.loop):
+ try:
+ self.refresh()
+ keys = self.get_key(duration=0.4)
+ if len(keys[0]) == 0:
+ self.is_refresh_header = True
+ self.refresh_header()
+ keys = self.get_key()
+
+ if keys == ["\x1B[23~"]:
+ keys = ["\x1B[15;5x"]
+ if keys == ["\x1B[24~"]:
+ keys = ["\x1B[20;5y"]
+
+ if self.trace is not None:
+ for key in keys:
+ self.trace.write(strings.dump(key, withColor=False) + "\n")
+ self.trace.flush()
+ modified = self.edit.text.modified
+ self.precedent_callback = self.key_callback
+ self.key_callback = None
+
+ if ord(keys[0][0]) < 0x20:
+ if keys[0] in self.EDIT_KEYS:
+ if keys[0] in self.cfg.key_toggle_mode: self.key_callback = self.toggle_mode
+ elif keys[0] in self.cfg.key_find: self.key_callback = self.find
+ elif keys[0] in self.cfg.key_replace: self.key_callback = self.replace
+ elif keys[0] in self.cfg.key_find_previous: self.key_callback = self.find_previous
+ elif keys[0] in self.cfg.key_find_next: self.key_callback = self.find_next
+ elif keys[0] in self.cfg.key_exit: self.key_callback = self.exit
+ elif keys[0] in self.cfg.key_goto: self.key_callback = self.goto
+ elif keys[0] in self.cfg.key_save: self.key_callback = self.save
+ elif keys[0] in self.cfg.key_execute: self.key_callback = self.execute
+
+ # If a replacement is in progress and new line pressed
+ if keys[0] in self.cfg.key_new_line:
+ if self.precedent_callback is not None:
+ # The next check is compatible with micropython
+ if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
+ if self.replace_text is not None:
+ # Replace current found
+ self.key_callback = self.replace_current
+ # If a replacement is in progress and select all pressed
+ if keys[0] in self.cfg.key_sel_all:
+ if self.precedent_callback is not None:
+ # The next check is compatible with micropython
+ if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
+ if self.replace_text is not None:
+ # Replace all
+ self.key_callback = self.replace_all
+
+ if self.key_callback is not None:
+ self.key_callback(keys)
+ else:
+ self.edit.text.treat_key(keys)
+ if modified != self.edit.text.modified:
+ self.is_refresh_header = True
+ except KeyboardInterrupt:
+ pass
+ self.edit.view.reset_scroll_region()
+ self.edit.view.reset()
+ except Exception as err:
+ print(logger.exception(err))
+ filename = self.edit.text.getFilename() + "_backup"
+ self.save(filename=filename)
+ print("After the crash, a copy of the file was saved in '%s'"%filename)
""" Refresh the header of editor """
if self.is_refresh_header:
self.edit.view.move_cursor(0, 0)
- filename_ = "\u25C1 File: %s"%(self.filename)
+ filename = "\u25C1 File: %s"%(self.displayed_filename)
if self.edit.text.read_only is False:
- filename_ += " (*)" if self.edit.text.modified else ""
+ filename += " (*)" if self.edit.text.modified else ""
end = " Mode: %s "%("Replace" if self.edit.text.replace_mode else "Insert")
else:
end = " Read only " if self.edit.text.read_only else ""
end = "L%d C%d "%(self.edit.text.cursor_line+1, self.edit.view.tab_cursor_column+1) + end + "\u25B7"
- header = "\x1B[7m%s%s%s\x1B[m"%(filename_, " "*(self.edit.view.width - len(filename_) - len(end)), end)
+ header = "\x1B[7m%s%s%s\x1B[m"%(filename, " "*(self.edit.view.width - len(filename) - len(end)), end)
self.edit.view.write(header)
self.edit.view.move_cursor()
self.is_refresh_header = False
@@ -2723,75 +2764,81 @@
Methods
def run(self):
""" Core of the editor """
- self.edit.view.cls()
- self.edit.view.get_screen_size()
- self.loop = True
- self.EDIT_KEYS = self.cfg.key_toggle_mode+self.cfg.key_find+self.cfg.key_replace+self.cfg.key_find_previous+self.cfg.key_find_next+self.cfg.key_exit+self.cfg.key_goto+self.cfg.key_save+self.cfg.key_execute
- while(self.loop):
- try:
- self.refresh()
- keys = self.get_key(duration=0.4)
- if len(keys[0]) == 0:
- self.is_refresh_header = True
- self.refresh_header()
- keys = self.get_key()
+ try:
+ self.edit.view.cls()
+ self.edit.view.get_screen_size()
+ self.loop = True
+ self.EDIT_KEYS = self.cfg.key_toggle_mode+self.cfg.key_find+self.cfg.key_replace+self.cfg.key_find_previous+self.cfg.key_find_next+self.cfg.key_exit+self.cfg.key_goto+self.cfg.key_save+self.cfg.key_execute
+ while(self.loop):
+ try:
+ self.refresh()
+ keys = self.get_key(duration=0.4)
+ if len(keys[0]) == 0:
+ self.is_refresh_header = True
+ self.refresh_header()
+ keys = self.get_key()
- if keys == ["\x1B[23~"]:
- keys = ["\x1B[1;5x"]
- if keys == ["\x1B[24~"]:
- keys = ["\x1B[5;1y"]
-
- if self.trace is not None:
- for key in keys:
- self.trace.write(strings.dump(key, withColor=False) + "\n")
- self.trace.flush()
- modified = self.edit.text.modified
- self.precedent_callback = self.key_callback
- self.key_callback = None
-
- if ord(keys[0][0]) < 0x20:
- if keys[0] in self.EDIT_KEYS:
- if keys[0] in self.cfg.key_toggle_mode: self.key_callback = self.toggle_mode
- elif keys[0] in self.cfg.key_find: self.key_callback = self.find
- elif keys[0] in self.cfg.key_replace: self.key_callback = self.replace
- elif keys[0] in self.cfg.key_find_previous: self.key_callback = self.find_previous
- elif keys[0] in self.cfg.key_find_next: self.key_callback = self.find_next
- elif keys[0] in self.cfg.key_exit: self.key_callback = self.exit
- elif keys[0] in self.cfg.key_goto: self.key_callback = self.goto
- elif keys[0] in self.cfg.key_save: self.key_callback = self.save
- elif keys[0] in self.cfg.key_execute: self.key_callback = self.execute
-
- # If a replacement is in progress and new line pressed
- if keys[0] in self.cfg.key_new_line:
- if self.precedent_callback is not None:
- # The next check is compatible with micropython
- if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
- if self.replace_text is not None:
- # Replace current found
- self.key_callback = self.replace_current
- # If a replacement is in progress and select all pressed
- if keys[0] in self.cfg.key_sel_all:
- if self.precedent_callback is not None:
- # The next check is compatible with micropython
- if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
- if self.replace_text is not None:
- # Replace all
- self.key_callback = self.replace_all
-
- if self.key_callback is not None:
- self.key_callback(keys)
- else:
- self.edit.text.treat_key(keys)
- if modified != self.edit.text.modified:
- self.is_refresh_header = True
- except KeyboardInterrupt:
- pass
- self.edit.view.reset_scroll_region()
- self.edit.view.reset()
+ if keys == ["\x1B[23~"]:
+ keys = ["\x1B[15;5x"]
+ if keys == ["\x1B[24~"]:
+ keys = ["\x1B[20;5y"]
+
+ if self.trace is not None:
+ for key in keys:
+ self.trace.write(strings.dump(key, withColor=False) + "\n")
+ self.trace.flush()
+ modified = self.edit.text.modified
+ self.precedent_callback = self.key_callback
+ self.key_callback = None
+
+ if ord(keys[0][0]) < 0x20:
+ if keys[0] in self.EDIT_KEYS:
+ if keys[0] in self.cfg.key_toggle_mode: self.key_callback = self.toggle_mode
+ elif keys[0] in self.cfg.key_find: self.key_callback = self.find
+ elif keys[0] in self.cfg.key_replace: self.key_callback = self.replace
+ elif keys[0] in self.cfg.key_find_previous: self.key_callback = self.find_previous
+ elif keys[0] in self.cfg.key_find_next: self.key_callback = self.find_next
+ elif keys[0] in self.cfg.key_exit: self.key_callback = self.exit
+ elif keys[0] in self.cfg.key_goto: self.key_callback = self.goto
+ elif keys[0] in self.cfg.key_save: self.key_callback = self.save
+ elif keys[0] in self.cfg.key_execute: self.key_callback = self.execute
+
+ # If a replacement is in progress and new line pressed
+ if keys[0] in self.cfg.key_new_line:
+ if self.precedent_callback is not None:
+ # The next check is compatible with micropython
+ if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
+ if self.replace_text is not None:
+ # Replace current found
+ self.key_callback = self.replace_current
+ # If a replacement is in progress and select all pressed
+ if keys[0] in self.cfg.key_sel_all:
+ if self.precedent_callback is not None:
+ # The next check is compatible with micropython
+ if self.precedent_callback.__name__ in [self.find_next.__name__, self.find_previous.__name__, self.replace.__name__, self.replace_current.__name__]:
+ if self.replace_text is not None:
+ # Replace all
+ self.key_callback = self.replace_all
+
+ if self.key_callback is not None:
+ self.key_callback(keys)
+ else:
+ self.edit.text.treat_key(keys)
+ if modified != self.edit.text.modified:
+ self.is_refresh_header = True
+ except KeyboardInterrupt:
+ pass
+ self.edit.view.reset_scroll_region()
+ self.edit.view.reset()
+ except Exception as err:
+ print(logger.exception(err))
+ filename = self.edit.text.getFilename() + "_backup"
+ self.save(filename=filename)
+ print("After the crash, a copy of the file was saved in '%s'"%filename)
logger.syslog(err)
self.lines = [""]
- def save(self):
+ def save(self, filename=None):
""" Save text in the file """
result = False
if self.read_only is False:
- if self.filename is not None:
+ if filename is None:
+ filename = self.filename
+ if filename is not None:
try:
- file = open(self.filename, "w")
+ file = open(filename, "w")
for line in self.lines:
file.write(line)
file.close()
@@ -3350,7 +3403,7 @@
Ancestors
def get_selection(self):
""" Get information about selection """
- if self.selection_start:
+ if self.selection_start is not None and self.selection_end is not None:
if self.selection_start[1] > self.selection_end[1]:
return self.selection_end, self.selection_start
elif self.selection_start[1] < self.selection_end[1]:
@@ -3736,7 +3789,7 @@
Ancestors
def change_case(self, keys=None):
""" Change the case of selection """
selection = self.copy_clipboard()
- if selection != []:
+ if len(selection) > 0:
self.modified = True
selection_start = self.selection_start
selection_end = self.selection_end
@@ -4021,21 +4074,19 @@
Ancestors
if len(keys[0]) > 3:
# If camflasher mouse selection
if keys[0][0:2] == "\x1B[" and keys[0][-1] in ["x","y"]:
- if keys[0][-1] == "y":
- end = True
- else:
-
+ try:
+ pos = keys[0][2:-1]
+ line, column = pos.split(";")
self.begin_line, self.begin_column = self.view.get_position()
- self.hide_selection()
- end = False
- pos = keys[0][2:-1]
- line, column = pos.split(";")
-
- if end:
- self.open_selection()
- self.goto(int(line)+self.begin_line,int(column)+self.begin_column, not end)
- if end:
- self.close_selection()
+ if keys[0][-1] == "x":
+ self.goto(int(line)+self.begin_line,int(column)+self.begin_column, True)
+ self.open_selection()
+ self.close_selection()
+ else:
+ self.goto(int(line)+self.begin_line,int(column)+self.begin_column, False)
+ self.close_selection()
+ except Exception as err:
+ pass
Methods
@@ -4186,7 +4237,7 @@
Methods
def change_case(self, keys=None):
""" Change the case of selection """
selection = self.copy_clipboard()
- if selection != []:
+ if len(selection) > 0:
self.modified = True
selection_start = self.selection_start
selection_end = self.selection_end
@@ -4636,6 +4687,20 @@
def get_selection(self):
""" Get information about selection """
- if self.selection_start:
+ if self.selection_start is not None and self.selection_end is not None:
if self.selection_start[1] > self.selection_end[1]:
return self.selection_end, self.selection_start
elif self.selection_start[1] < self.selection_end[1]:
@@ -4982,7 +5047,7 @@
def load(self, filename):
""" Load file in the editor """
self.filename = None
try:
self.lines = []
- self.filename = filename_
- file = open(filename_, "r")
+ self.filename = filename
+ file = open(filename, "r")
line = file.readline()
while line != "":
self.lines.append(line.replace("\r\n","\n"))
@@ -5323,7 +5388,7 @@
Methods
-def save(self)
+def save(self, filename=None)
Save text in the file
@@ -5331,13 +5396,15 @@
Methods
Expand source code
-
def save(self):
+
def save(self, filename=None):
""" Save text in the file """
result = False
if self.read_only is False:
- if self.filename is not None:
+ if filename is None:
+ filename = self.filename
+ if filename is not None:
try:
- file = open(self.filename, "w")
+ file = open(filename, "w")
for line in self.lines:
file.write(line)
file.close()
@@ -5720,21 +5787,19 @@
Methods
if len(keys[0]) > 3:
# If camflasher mouse selection
if keys[0][0:2] == "\x1B[" and keys[0][-1] in ["x","y"]:
- if keys[0][-1] == "y":
- end = True
- else:
-
+ try:
+ pos = keys[0][2:-1]
+ line, column = pos.split(";")
self.begin_line, self.begin_column = self.view.get_position()
- self.hide_selection()
- end = False
- pos = keys[0][2:-1]
- line, column = pos.split(";")
-
- if end:
- self.open_selection()
- self.goto(int(line)+self.begin_line,int(column)+self.begin_column, not end)
- if end:
- self.close_selection()
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-iterating-dictionary
""" Syntax highlight for python in the editor """
PYTHON_KEYWORDS = b"and as assert break class continue def del elif else except exec finally for from global if import in is lambda None not or pass print raise return try while self as join abs apply bool buffer callable chr cmp coerce compile complex delattr dir dict divmod eval execfile filter float getattr globals hasattr hash hex id input int intern isinstance issubclass len list locals long map max min oct open ord pow range raw_input reduce reload repr round setattr slice str tuple type unichr unicode vars xrange zip with yield True False async await"
@@ -80,10 +81,11 @@
Module lib.shell.editor_py
pos = -1
# If a keyword start
if char in self.lexicon.keys():
- pos = j
- state = STATE_KEYWORD
- word = charactere
- keywords = self.lexicon[char]
+ if not (0x41 <= previous_char <= 0x5A or 0x61 <= previous_char <= 0x7A or 0x30 <= previous_char <= 0x39 or previous_char == 0x5F):
+ pos = j
+ state = STATE_KEYWORD
+ word = charactere
+ keywords = self.lexicon[char]
# If decimal number started
elif 0x31 <= char <= 0x39:
if 0x41 <= previous_char <= 0x5A or 0x61 <= previous_char <= 0x7A or 0x30 <= previous_char <= 0x39 or previous_char == 0x5F:
@@ -189,7 +191,7 @@
Module lib.shell.editor_py
keywords = None
# If decimal detected
elif state == STATE_DECIMAL:
- if char >= 0x30 and char <= 0x39:
+ if char >= 0x30 and char <= 0x39 or char == 0x5F:
word += charactere
elif char == 0x2E:
if b"." not in word:
@@ -396,10 +398,11 @@
Classes
pos = -1
# If a keyword start
if char in self.lexicon.keys():
- pos = j
- state = STATE_KEYWORD
- word = charactere
- keywords = self.lexicon[char]
+ if not (0x41 <= previous_char <= 0x5A or 0x61 <= previous_char <= 0x7A or 0x30 <= previous_char <= 0x39 or previous_char == 0x5F):
+ pos = j
+ state = STATE_KEYWORD
+ word = charactere
+ keywords = self.lexicon[char]
# If decimal number started
elif 0x31 <= char <= 0x39:
if 0x41 <= previous_char <= 0x5A or 0x61 <= previous_char <= 0x7A or 0x30 <= previous_char <= 0x39 or previous_char == 0x5F:
@@ -505,7 +508,7 @@
Classes
keywords = None
# If decimal detected
elif state == STATE_DECIMAL:
- if char >= 0x30 and char <= 0x39:
+ if char >= 0x30 and char <= 0x39 or char == 0x5F:
word += charactere
elif char == 0x2E:
if b"." not in word:
@@ -693,10 +696,11 @@
Methods
pos = -1
# If a keyword start
if char in self.lexicon.keys():
- pos = j
- state = STATE_KEYWORD
- word = charactere
- keywords = self.lexicon[char]
+ if not (0x41 <= previous_char <= 0x5A or 0x61 <= previous_char <= 0x7A or 0x30 <= previous_char <= 0x39 or previous_char == 0x5F):
+ pos = j
+ state = STATE_KEYWORD
+ word = charactere
+ keywords = self.lexicon[char]
# If decimal number started
elif 0x31 <= char <= 0x39:
if 0x41 <= previous_char <= 0x5A or 0x61 <= previous_char <= 0x7A or 0x30 <= previous_char <= 0x39 or previous_char == 0x5F:
@@ -802,7 +806,7 @@
Methods
keywords = None
# If decimal detected
elif state == STATE_DECIMAL:
- if char >= 0x30 and char <= 0x39:
+ if char >= 0x30 and char <= 0x39 or char == 0x5F:
word += charactere
elif char == 0x2E:
if b"." not in word:
diff --git a/doc/lib/shell/shell.html b/doc/lib/shell/shell.html
index 36e6c1f..2cd37b7 100644
--- a/doc/lib/shell/shell.html
+++ b/doc/lib/shell/shell.html
@@ -110,6 +110,9 @@
Module lib.shell.shell
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=too-many-lines
+# pylint:disable=consider-using-f-string
+# pylint:disable=unspecified-encoding
""" Class defining a minimalist shell, directly executable on the board.
We modify directories, list, delete, move files, edit files ...
The commands are :
@@ -161,12 +164,13 @@
Module lib.shell.shell
import os
import uos
import machine
-from tools import useful,logger,sdcard,filesystem,exchange,info,strings,terminal,watchdog
+from tools import useful,logger,sdcard,filesystem,exchange,info,strings,terminal,watchdog,lang,date
stdout_redirected = None
def print_(message, end=None):
""" Redirect the print to file """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if stdout_redirected is None:
if end is None:
@@ -182,6 +186,7 @@
Module lib.shell.shell
def get_screen_size():
""" Return the screen size and check if output redirected """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if stdout_redirected is None:
height, width = terminal.get_screen_size()
@@ -369,7 +374,7 @@
def print_part(message, width, height, count):
""" Print a part of text """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if isinstance(message , bytes):
message = message.decode("utf8")
@@ -542,7 +548,7 @@
Module lib.shell.shell
except:
print_("Cannot umount sd from '%s'"%mountpoint)
-def date(update=False, offsetUTC=+1, noDst=False):
+def date_(update=False, offsetUTC=+1, noDst=False):
""" Get or set date """
try:
from server.timesetting import set_date
@@ -555,7 +561,7 @@
Module lib.shell.shell
del sys.modules["server.timesetting"]
except:
pass
- print_(strings.date_to_string())
+ print_(date.date_to_string())
def setdate(datetime=""):
""" Set date and time """
@@ -620,9 +626,14 @@
Module lib.shell.shell
""" Deep sleep command """
machine.deepsleep(int(seconds)*1000)
+def ligthsleep(seconds=60):
+ """ Light sleep command """
+ machine.lightsleep(int(seconds)*1000)
+
edit_class = None
def edit(file, no_color=False, read_only=False):
""" Edit command """
+ # pylint:disable=global-variable-not-assigned
global edit_class
global stdout_redirected
if stdout_redirected is None:
@@ -654,7 +665,7 @@
def uptime():
""" Tell how long the system has been running """
- print_(info.uptime())
+ print_(strings.tostrings(info.uptime()))
def man(command):
""" Man command """
@@ -742,6 +753,7 @@
Module lib.shell.shell
def check_cam_flasher():
""" Check if the terminal is CamFlasher """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if stdout_redirected is None:
# Request terminal device attribut
@@ -823,15 +835,15 @@
Module lib.shell.shell
def meminfo():
""" Get memory informations """
- print_(strings.tostrings(info.meminfo(display=False)))
+ print_(strings.tostrings(b"%s : %s"%(lang.memory_label, info.meminfo())))
def flashinfo(mountpoint=None):
""" Get flash informations """
- print_(strings.tostrings(info.flashinfo(mountpoint=mountpoint, display=False)))
+ print_(strings.tostrings(b"%s : %s"%(lang.flash_info, info.flashinfo(mountpoint=mountpoint))))
def sysinfo():
""" Get system informations """
- print_(strings.tostrings(info.sysinfo(display=False)))
+ print_(strings.tostrings(info.sysinfo()))
def vtcolors():
""" Show all VT100 colors """
@@ -872,6 +884,7 @@
Module lib.shell.shell
def get_command(command_name):
""" Get a command callback according to the command name """
try:
+ # pylint:disable=global-variable-not-assigned
global shell_commands
command = shell_commands[command_name]
command_function = command[0]
@@ -889,6 +902,7 @@
def check_cam_flasher():
""" Check if the terminal is CamFlasher """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if stdout_redirected is None:
# Request terminal device attribut
@@ -1350,8 +1366,8 @@
def get_command(command_name):
""" Get a command callback according to the command name """
try:
+ # pylint:disable=global-variable-not-assigned
global shell_commands
command = shell_commands[command_name]
command_function = command[0]
@@ -1715,6 +1734,7 @@
Functions
def get_screen_size():
""" Return the screen size and check if output redirected """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if stdout_redirected is None:
height, width = terminal.get_screen_size()
@@ -1845,6 +1865,20 @@
def print_(message, end=None):
""" Redirect the print to file """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if stdout_redirected is None:
if end is None:
@@ -2086,6 +2121,7 @@
Functions
def print_part(message, width, height, count):
""" Print a part of text """
+ # pylint:disable=global-variable-not-assigned
global stdout_redirected
if isinstance(message , bytes):
message = message.decode("utf8")
@@ -2391,7 +2427,7 @@
Functions
def sysinfo():
""" Get system informations """
- print_(strings.tostrings(info.sysinfo(display=False)))
+ print_(strings.tostrings(info.sysinfo()))
@@ -2467,7 +2503,7 @@
Functions
def uptime():
""" Tell how long the system has been running """
- print_(info.uptime())
Battery.config.save()
# if the number of consecutive brownout resets is too high
- if Battery.config.brownout_count > 32:
+ if Battery.config.brownout_count > MAX_BROWNOUT_RESET:
# Battery too low, save the battery status
- logger.syslog("Too many successive brownout reset")
+ logger.syslog("Too many successive brownout reset %d"%Battery.config.brownout_count)
deepsleep = True
return deepsleep
@@ -567,9 +611,9 @@
Static methods
Battery.config.save()
# if the number of consecutive brownout resets is too high
- if Battery.config.brownout_count > 32:
+ if Battery.config.brownout_count > MAX_BROWNOUT_RESET:
# Battery too low, save the battery status
- logger.syslog("Too many successive brownout reset")
+ logger.syslog("Too many successive brownout reset %d"%Battery.config.brownout_count)
deepsleep = True
return deepsleep
# Distributed under MIT License
+# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
+""" Date and time utilities """
+import sys
+import time
+
+def local_time(date_=None):
+ """ Safe local time, it return 2000/1/1 00:00:00 if date can be extracted """
+ try:
+ year,month,day,hour,minute,second,weekday,yearday = time.localtime(date_)[:8]
+ except:
+ year,month,day,hour,minute,second,weekday,yearday = 2000,1,1,0,0,0,0,0
+ return year,month,day,hour,minute,second,weekday,yearday
+
+def date_to_string(date_ = None):
+ """ Get a string with the current date """
+ return date_to_bytes(date_).decode("utf8")
+
+def date_to_bytes(date_ = None):
+ """ Get a bytes with the current date """
+ year,month,day,hour,minute,second = local_time(date_)[:6]
+ return b"%04d/%02d/%02d %02d:%02d:%02d"%(year,month,day,hour,minute,second)
+
+def date_ms_to_string():
+ """ Get a string with the current date with ms """
+ current = time.time_ns()
+ ms = (current // 1_000_000)%1000
+ current //= 1_000_000_000
+ year,month,day,hour,minute,second = local_time(current)[:6]
+ return "%04d/%02d/%02d %02d:%02d:%02d.%03d"%(year,month,day,hour,minute,second,ms)
+
+def mktime(t):
+ """ Portable mktime """
+ year,month,day,hour,minute,second,weekday,yearday = t
+ if sys.implementation.name == "micropython":
+ result = time.mktime((year, month, day, hour, minute, second, weekday, yearday))
+ else:
+ result = time.mktime((year, month, day, hour, minute, second, weekday, yearday, 0))
+ return result
+
+def html_to_date(date_, separator=b"-"):
+ """ Convert html date into time integer """
+ result = 0
+ try:
+ year, month, day = date_.split(separator)
+ year = int( year.lstrip(b"0"))
+ month = int(month.lstrip(b"0"))
+ day = int( day.lstrip(b"0"))
+ result = mktime((year, month, day, 1,0,0, 0,0))
+ except:
+ result = (time.time() // 86400) * 86400
+ return result
+
+def date_to_html(date_ = None, separator=b"-"):
+ """ Get a html date with the current date """
+ year,month,day = local_time(date_)[:3]
+ return b"%04d%s%02d%s%02d"%(year,separator,month,separator,day)
+
+
+def html_to_time(time_, separator=b":"):
+ """ Convert html time string into time integer """
+ result = 0
+ try:
+ hour, minute, second = time_.split(separator)
+ hour = int( hour.lstrip(b"0"))
+ minute = int(minute.lstrip(b"0"))
+ second = int(second.lstrip(b"0"))
+ result = hour * 3600 + minute * 60 * second
+ except:
+ try:
+ hour, minute = time_.split(separator)
+ hour = int( hour.lstrip(b"0"))
+ minute = int(minute.lstrip(b"0"))
+ result = hour * 3600 + minute * 60
+ except:
+ pass
+ return result
+
+def time_to_html(t = None, seconds=False):
+ """ Convert time into date bytes """
+ if t is None:
+ t = time.time()
+ t %= 86400
+ result = b"%02d:%02d:%02d"%(t//3600, (t%3600)//60, t%60)
+ if seconds is False:
+ result = result[:-3]
+ return result
+
+def date_to_filename(date_ = None):
+ """ Get a filename with a date """
+ filename = date_to_string(date_)
+ filename = filename.replace(" "," ")
+ filename = filename.replace(" ","_")
+ filename = filename.replace("/","-")
+ filename = filename.replace(":","-")
+ return filename
+
+def date_to_path(date_=None):
+ """ Get a path with year/month/day/hour """
+ year,month,day,hour,minute = local_time(date_)[:5]
+ return b"%04d/%02d/%02d/%02dh%02d"%(year,month,day,hour,minute)
+
+
+
+
+
+
+
+
Functions
+
+
+def date_ms_to_string()
+
+
+
Get a string with the current date with ms
+
+
+Expand source code
+
+
def date_ms_to_string():
+ """ Get a string with the current date with ms """
+ current = time.time_ns()
+ ms = (current // 1_000_000)%1000
+ current //= 1_000_000_000
+ year,month,day,hour,minute,second = local_time(current)[:6]
+ return "%04d/%02d/%02d %02d:%02d:%02d.%03d"%(year,month,day,hour,minute,second,ms)
+
+
+
+def date_to_bytes(date_=None)
+
+
+
Get a bytes with the current date
+
+
+Expand source code
+
+
def date_to_bytes(date_ = None):
+ """ Get a bytes with the current date """
+ year,month,day,hour,minute,second = local_time(date_)[:6]
+ return b"%04d/%02d/%02d %02d:%02d:%02d"%(year,month,day,hour,minute,second)
+
+
+
+def date_to_filename(date_=None)
+
+
+
Get a filename with a date
+
+
+Expand source code
+
+
def date_to_filename(date_ = None):
+ """ Get a filename with a date """
+ filename = date_to_string(date_)
+ filename = filename.replace(" "," ")
+ filename = filename.replace(" ","_")
+ filename = filename.replace("/","-")
+ filename = filename.replace(":","-")
+ return filename
+
+
+
+def date_to_html(date_=None, separator=b'-')
+
+
+
Get a html date with the current date
+
+
+Expand source code
+
+
def date_to_html(date_ = None, separator=b"-"):
+ """ Get a html date with the current date """
+ year,month,day = local_time(date_)[:3]
+ return b"%04d%s%02d%s%02d"%(year,separator,month,separator,day)
+
+
+
+def date_to_path(date_=None)
+
+
+
Get a path with year/month/day/hour
+
+
+Expand source code
+
+
def date_to_path(date_=None):
+ """ Get a path with year/month/day/hour """
+ year,month,day,hour,minute = local_time(date_)[:5]
+ return b"%04d/%02d/%02d/%02dh%02d"%(year,month,day,hour,minute)
+
+
+
+def date_to_string(date_=None)
+
+
+
Get a string with the current date
+
+
+Expand source code
+
+
def date_to_string(date_ = None):
+ """ Get a string with the current date """
+ return date_to_bytes(date_).decode("utf8")
+
+
+
+def html_to_date(date_, separator=b'-')
+
+
+
Convert html date into time integer
+
+
+Expand source code
+
+
def html_to_date(date_, separator=b"-"):
+ """ Convert html date into time integer """
+ result = 0
+ try:
+ year, month, day = date_.split(separator)
+ year = int( year.lstrip(b"0"))
+ month = int(month.lstrip(b"0"))
+ day = int( day.lstrip(b"0"))
+ result = mktime((year, month, day, 1,0,0, 0,0))
+ except:
+ result = (time.time() // 86400) * 86400
+ return result
+
+
+
+def html_to_time(time_, separator=b':')
+
+
+
Convert html time string into time integer
+
+
+Expand source code
+
+
def html_to_time(time_, separator=b":"):
+ """ Convert html time string into time integer """
+ result = 0
+ try:
+ hour, minute, second = time_.split(separator)
+ hour = int( hour.lstrip(b"0"))
+ minute = int(minute.lstrip(b"0"))
+ second = int(second.lstrip(b"0"))
+ result = hour * 3600 + minute * 60 * second
+ except:
+ try:
+ hour, minute = time_.split(separator)
+ hour = int( hour.lstrip(b"0"))
+ minute = int(minute.lstrip(b"0"))
+ result = hour * 3600 + minute * 60
+ except:
+ pass
+ return result
+
+
+
+def local_time(date_=None)
+
+
+
Safe local time, it return 2000/1/1 00:00:00 if date can be extracted
+
+
+Expand source code
+
+
def local_time(date_=None):
+ """ Safe local time, it return 2000/1/1 00:00:00 if date can be extracted """
+ try:
+ year,month,day,hour,minute,second,weekday,yearday = time.localtime(date_)[:8]
+ except:
+ year,month,day,hour,minute,second,weekday,yearday = 2000,1,1,0,0,0,0,0
+ return year,month,day,hour,minute,second,weekday,yearday
+
+
+
+def mktime(t)
+
+
+
Portable mktime
+
+
+Expand source code
+
+
def mktime(t):
+ """ Portable mktime """
+ year,month,day,hour,minute,second,weekday,yearday = t
+ if sys.implementation.name == "micropython":
+ result = time.mktime((year, month, day, hour, minute, second, weekday, yearday))
+ else:
+ result = time.mktime((year, month, day, hour, minute, second, weekday, yearday, 0))
+ return result
+
+
+
+def time_to_html(t=None, seconds=False)
+
+
+
Convert time into date bytes
+
+
+Expand source code
+
+
def time_to_html(t = None, seconds=False):
+ """ Convert time into date bytes """
+ if t is None:
+ t = time.time()
+ t %= 86400
+ result = b"%02d:%02d:%02d"%(t//3600, (t%3600)//60, t%60)
+ if seconds is False:
+ result = result[:-3]
+ return result
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/tools/exchange.html b/doc/lib/tools/exchange.html
index 388e43f..7ac4b43 100644
--- a/doc/lib/tools/exchange.html
+++ b/doc/lib/tools/exchange.html
@@ -29,6 +29,7 @@
Module lib.tools.exchange
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Classes for exchanging files between the device and the computer """
import time
import os
@@ -36,9 +37,9 @@
Module lib.tools.exchange
import binascii
try:
import filesystem
- import strings
+ import date
except:
- from tools import filesystem, strings
+ from tools import filesystem, date
if filesystem.ismicropython():
# pylint:disable=import-error
import micropython
@@ -145,8 +146,8 @@
Module lib.tools.exchange
def read_second(self, byte):
""" Read second """
if self.second.read_byte(byte) is not None:
- date = [self.year.get(), self.month.get(), self.day.get(), self.hour.get(), self.minute.get(), self.second.get(), 0, 0, 0]
- self.value = time.mktime(tuple(date))
+ date_ = [self.year.get(), self.month.get(), self.day.get(), self.hour.get(), self.minute.get(), self.second.get(), 0, 0, 0]
+ self.value = time.mktime(tuple(date_))
return self.value
class FilenameReader(Reader):
@@ -465,7 +466,7 @@
Module lib.tools.exchange
out_file.write(b"# %s\x0D\x0A"%filename_.encode("utf8"))
# Send the file date
- year,month,day,hour,minute,second,_,_ = strings.local_time(filesystem.filetime(filename))[:8]
+ year,month,day,hour,minute,second,_,_ = date.local_time(filesystem.filetime(filename))[:8]
out_file.write(b"# %04d/%02d/%02d %02d:%02d:%02d\x0D\x0A"%(year,month,day,hour,minute,second))
# Send the file size
@@ -879,8 +880,8 @@
Inherited members
def read_second(self, byte):
""" Read second """
if self.second.read_byte(byte) is not None:
- date = [self.year.get(), self.month.get(), self.day.get(), self.hour.get(), self.minute.get(), self.second.get(), 0, 0, 0]
- self.value = time.mktime(tuple(date))
+ date_ = [self.year.get(), self.month.get(), self.day.get(), self.hour.get(), self.minute.get(), self.second.get(), 0, 0, 0]
+ self.value = time.mktime(tuple(date_))
return self.value
Ancestors
@@ -961,8 +962,8 @@
Methods
def read_second(self, byte):
""" Read second """
if self.second.read_byte(byte) is not None:
- date = [self.year.get(), self.month.get(), self.day.get(), self.hour.get(), self.minute.get(), self.second.get(), 0, 0, 0]
- self.value = time.mktime(tuple(date))
+ date_ = [self.year.get(), self.month.get(), self.day.get(), self.hour.get(), self.minute.get(), self.second.get(), 0, 0, 0]
+ self.value = time.mktime(tuple(date_))
return self.value
@@ -1495,7 +1496,7 @@
Methods
out_file.write(b"# %s\x0D\x0A"%filename_.encode("utf8"))
# Send the file date
- year,month,day,hour,minute,second,_,_ = strings.local_time(filesystem.filetime(filename))[:8]
+ year,month,day,hour,minute,second,_,_ = date.local_time(filesystem.filetime(filename))[:8]
out_file.write(b"# %04d/%02d/%02d %02d:%02d:%02d\x0D\x0A"%(year,month,day,hour,minute,second))
# Send the file size
@@ -1586,7 +1587,7 @@
Methods
out_file.write(b"# %s\x0D\x0A"%filename_.encode("utf8"))
# Send the file date
- year,month,day,hour,minute,second,_,_ = strings.local_time(filesystem.filetime(filename))[:8]
+ year,month,day,hour,minute,second,_,_ = date.local_time(filesystem.filetime(filename))[:8]
out_file.write(b"# %04d/%02d/%02d %02d:%02d:%02d\x0D\x0A"%(year,month,day,hour,minute,second))
# Send the file size
diff --git a/doc/lib/tools/filesystem.html b/doc/lib/tools/filesystem.html
index 0a21384..a1df4e9 100644
--- a/doc/lib/tools/filesystem.html
+++ b/doc/lib/tools/filesystem.html
@@ -39,6 +39,7 @@
if path == "":
path = "."
if path is not None and pattern is not None:
- for file in os.listdir(path):
+ for file_info in uos.ilistdir(path):
+ name = file_info[0]
+ typ = file_info[1]
if path != "":
- filename = path + "/" + file
+ filename = path + "/" + name
else:
- filename = file
+ filename = name
if sys.platform != "win32":
filename = filename.replace("//","/")
filename = filename.replace("//","/")
- if isdir(filename):
+
+ # if directory
+ if typ & 0xF000 == 0x4000:
if displayer:
displayer.show(filename)
else:
@@ -226,12 +231,51 @@
Module lib.tools.filesystem
filenames += fils
directories += dirs
else:
- if fnmatch.fnmatch(file, pattern):
+ if fnmatch.fnmatch(name, pattern):
+ if displayer:
+ displayer.show(filename)
+ filenames = [""]
+ else:
+ filenames.append(filename)
+ return directories, filenames
+
+async def ascandir(path, pattern, recursive, displayer=None):
+ """ Asynchronous scan recursively a directory """
+ filenames = []
+ directories = []
+ if path == "":
+ path = "."
+ if path is not None and pattern is not None:
+ for file_info in uos.ilistdir(path):
+ name = file_info[0]
+ typ = file_info[1]
+ if path != "":
+ filename = path + "/" + name
+ else:
+ filename = name
+ if sys.platform != "win32":
+ filename = filename.replace("//","/")
+ filename = filename.replace("//","/")
+
+ # if directory
+ if typ & 0xF000 == 0x4000:
+ if displayer:
+ displayer.show(filename)
+ else:
+ directories.append(filename)
+ if recursive:
+ dirs,fils = await ascandir(filename, pattern, recursive, displayer)
+ filenames += fils
+ directories += dirs
+ else:
+ if fnmatch.fnmatch(name, pattern):
if displayer:
displayer.show(filename)
filenames = [""]
else:
filenames.append(filename)
+ if ismicropython():
+ await uasyncio.sleep_ms(3)
return directories, filenames
def prefix(files):
@@ -381,6 +425,55 @@
if path == "":
path = "."
if path is not None and pattern is not None:
- for file in os.listdir(path):
+ for file_info in uos.ilistdir(path):
+ name = file_info[0]
+ typ = file_info[1]
if path != "":
- filename = path + "/" + file
+ filename = path + "/" + name
else:
- filename = file
+ filename = name
if sys.platform != "win32":
filename = filename.replace("//","/")
filename = filename.replace("//","/")
- if isdir(filename):
+
+ # if directory
+ if typ & 0xF000 == 0x4000:
if displayer:
displayer.show(filename)
else:
@@ -703,7 +800,7 @@
Functions
filenames += fils
directories += dirs
else:
- if fnmatch.fnmatch(file, pattern):
+ if fnmatch.fnmatch(name, pattern):
if displayer:
displayer.show(filename)
filenames = [""]
@@ -745,18 +842,18 @@
alloc = gc.mem_alloc()
free = gc.mem_free()
total = alloc+free
- result = b"Memory : alloc=%s free=%s total=%s used=%-3.2f%%"%(
+ result = lang.memory_info%(
strings.size_to_bytes(alloc, 1),
strings.size_to_bytes(free, 1),
strings.size_to_bytes(total, 1),
100-(free*100/total))
- if display:
- print(strings.tostrings(result))
- else:
- return result
except:
- return b"Mem unavailable"
+ result = lang.no_information
+ return result
-def flashinfo(mountpoint=None, display=True):
+def flash_size(mountpoint=None):
""" Get flash informations """
- try:
- import uos
- if mountpoint is None:
- mountpoint = os.getcwd()
- status = uos.statvfs(mountpoint)
+ import uos
+ if mountpoint is None:
+ mountpoint = os.getcwd()
+ status = uos.statvfs(mountpoint)
+ if filesystem.ismicropython():
free = status[0]*status[3]
if free < 0:
free = 0
total = status[1]*status[2]
alloc = total - free
- result = b"Disk %s : alloc=%s free=%s total=%s used=%-3.2f%%"%(strings.tobytes(mountpoint),
- strings.size_to_string(alloc, 1),
- strings.size_to_string(free, 1),
- strings.size_to_string(total, 1),
- 100-(free*100/total))
- if display:
- print(strings.tostrings(result))
- else:
- return result
+ else:
+ free = status.f_bavail * status.f_frsize
+ total = status.f_blocks * status.f_frsize
+ alloc = (status.f_blocks - status.f_bfree) * status.f_frsize
+ return total, alloc, free
+
+
+def flashinfo(mountpoint=None):
+ """ Get flash informations """
+ try:
+ total, alloc, free = flash_size(mountpoint=mountpoint)
except:
- return b"Flash unavailable"
+ alloc = 1
+ free = 1
+ total = alloc+free
+ percent = 100-(free*100/total)
+ total = strings.size_to_bytes(total, 1)
+ free = strings.size_to_bytes(free, 1)
+ alloc = strings.size_to_bytes(alloc, 1)
+ if mountpoint is None:
+ mountpoint = b"/"
+ else:
+ mountpoint = strings.tobytes(mountpoint)
+ result = lang.flash_info%(mountpoint, alloc, free, total, percent)
+ return result
-def sysinfo(display=True, text=""):
- """ Get system informations """
+def deviceinfo():
+ """ Get device informations """
try:
- result = b"Device : %s%s %dMhz\nTime : %s\n%s\n%s"%(text, sys.platform, machine.freq()//1000000, strings.date_to_bytes(), meminfo(False), flashinfo("/",False))
- if display:
- print(strings.tostrings(result))
- else:
- return result
+ return b"%s %dMhz"%(strings.tobytes(sys.platform), machine.freq()//1000000)
except:
+ return b"Device information unavailable"
+
+def sysinfo():
+ """ Get system informations """
+ try:
+ result = b"Device : %s\n"%(deviceinfo())
+ result += b"Time : %s\n"%(date.date_to_bytes())
+ result += b"%s : %s\n"%(lang.memory_label, meminfo())
+ result += b"%s : %s"%(lang.flash_label, flashinfo("/"))
+ return result
+ except Exception as err:
return b"Sysinfo not available"
up_last=None
@@ -137,34 +157,41 @@
Module lib.tools.info
up += up_total
return up
-def uptime(text="days"):
+def uptime(text=b"days"):
""" Tell how long the system has been running """
- up = uptime_sec()
- seconds = (up)%60
- mins = (up/60)%60
- hours = (up/3600)%24
- days = (up/86400)
- return "%d %s, %d:%02d:%02d"%(days, strings.tostrings(text),hours,mins,seconds)
+ try:
+ up = uptime_sec()
+ seconds = (up)%60
+ mins = (up/60)%60
+ hours = (up/3600)%24
+ days = (up/86400)
+ return b"%d %s, %d:%02d:%02d"%(days, text,hours,mins,seconds)
+ except:
+ return lang.no_information
_last_activity = 0
def get_last_activity():
""" Get the last activity from user """
+ # pylint:disable=global-variable-not-assigned
global _last_activity
return _last_activity
def set_last_activity():
""" Set the last activity from user """
+ # pylint:disable=global-variable-not-assigned
global _last_activity
_last_activity = uptime_sec()
_issues_counter = 0
def increase_issues_counter():
""" Increases a issue counter, that may require a reboot if there are too many"""
+ # pylint:disable=global-variable-not-assigned
global _issues_counter
_issues_counter += 1
def get_issues_counter():
""" Return the value of the issues counter """
+ # pylint:disable=global-variable-not-assigned
global _issues_counter
return _issues_counter
+ alloc = 1
+ free = 1
+ total = alloc+free
+ percent = 100-(free*100/total)
+ total = strings.size_to_bytes(total, 1)
+ free = strings.size_to_bytes(free, 1)
+ alloc = strings.size_to_bytes(alloc, 1)
+ if mountpoint is None:
+ mountpoint = b"/"
+ else:
+ mountpoint = strings.tobytes(mountpoint)
+ result = lang.flash_info%(mountpoint, alloc, free, total, percent)
+ return result
@@ -221,6 +288,7 @@
Functions
def get_issues_counter():
""" Return the value of the issues counter """
+ # pylint:disable=global-variable-not-assigned
global _issues_counter
return _issues_counter
@@ -236,6 +304,7 @@
Functions
def get_last_activity():
""" Get the last activity from user """
+ # pylint:disable=global-variable-not-assigned
global _last_activity
return _last_activity
@@ -251,6 +320,7 @@
Functions
def increase_issues_counter():
""" Increases a issue counter, that may require a reboot if there are too many"""
+ # pylint:disable=global-variable-not-assigned
global _issues_counter
_issues_counter += 1
alloc = gc.mem_alloc()
free = gc.mem_free()
total = alloc+free
- result = b"Memory : alloc=%s free=%s total=%s used=%-3.2f%%"%(
+ result = lang.memory_info%(
strings.size_to_bytes(alloc, 1),
strings.size_to_bytes(free, 1),
strings.size_to_bytes(total, 1),
100-(free*100/total))
- if display:
- print(strings.tostrings(result))
- else:
- return result
except:
- return b"Mem unavailable"
+ result = lang.no_information
+ return result
@@ -317,12 +384,13 @@
Functions
def set_last_activity():
""" Set the last activity from user """
+ # pylint:disable=global-variable-not-assigned
global _last_activity
_last_activity = uptime_sec()
def uptime(text=b"days"):
""" Tell how long the system has been running """
- up = uptime_sec()
- seconds = (up)%60
- mins = (up/60)%60
- hours = (up/3600)%24
- days = (up/86400)
- return "%d %s, %d:%02d:%02d"%(days, strings.tostrings(text),hours,mins,seconds)
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Functions for managing the json configuration.
All configuration classes end the name with the word Config.
For each of these classes, a json file with the same name is stored in the config directory of the board. """
@@ -44,11 +45,12 @@
Module lib.tools.jsonconfig
import uos
except:
import os as uos
+
try:
- from tools import logger,strings,filesystem
-except:
+ from tools import logger,strings,filesystem,date
+except Exception as err:
# pylint:disable=multiple-imports
- import logger,strings,filesystem
+ import logger,strings,filesystem,date
self_config = None
@@ -72,7 +74,7 @@
""" Convert the configuration to string """
data = self.__dict__.copy()
del data["modification_date"]
- return json.dumps(strings.tostrings(data))
+ return json.dumps(strings.tostrings(data),separators=(',', ':'))
def get_pathname(self, part_filename=""):
""" Get the configuration filename according to the class name """
@@ -112,6 +114,7 @@
Module lib.tools.jsonconfig
def open(self, file=None, read_write="r", part_filename=""):
""" Create or open configuration file """
+ # pylint:disable=unspecified-encoding
filename = file
if filesystem.exists(self.config_root()) is False:
filesystem.makedir(self.config_root())
@@ -122,7 +125,7 @@
Module lib.tools.jsonconfig
file = open(filename, read_write)
return file, filename
- def update(self, params):
+ def update(self, params, show_error=True):
""" Update object with html request params """
global self_config
if b"name" in params and b"value" in params and len(params) == 2:
@@ -131,6 +134,7 @@
Module lib.tools.jsonconfig
else:
setmany = True
self_config = self
+
for name in self.__dict__.keys():
# Case of web input is missing when bool is false
if type(self.__dict__[name]) == type(True):
@@ -154,13 +158,32 @@
Module lib.tools.jsonconfig
if setmany:
params[name] = False
# Case of web input is integer but string with number received
- elif type(self.__dict__[name]) == type(0) or type(self.__dict__[name]) == type(0.):
+ elif type(self.__dict__[name]) == type(0):
name = strings.tobytes(name)
if name in params:
try:
params[name] = int(params[name])
+ except:
+ if b"date" in name:
+ try:
+ params[name] = date.html_to_date(params[name])
+ except:
+ params[name] = 0
+ elif b"time" in name:
+ try:
+ params[name] = date.html_to_time(params[name])
+ except:
+ params[name] = 0
+ else:
+ params[name] = 0
+ elif type(self.__dict__[name]) == type(0.):
+ name = strings.tobytes(name)
+ if name in params:
+ try:
+ params[name] = float(params[name])
except:
params[name] = 0
+
result = True
for name, value in params.items():
execval = strings.tostrings(name)
@@ -169,7 +192,9 @@
Module lib.tools.jsonconfig
# pylint: disable=exec-used
exec("a = self_config.%s"%execval)
existing = True
- except:
+ except Exception as err:
+ if "'NoneType' object" in str(err):
+ result = None
existing = False
if existing:
@@ -177,16 +202,17 @@
Module lib.tools.jsonconfig
# pylint: disable=exec-used
exec(execval)
else:
- if name != b"action":
+ if name != b"action" and show_error and result is not None:
print("%s.%s not existing"%(self.__class__.__name__, strings.tostrings(name)))
except Exception as err:
logger.syslog(err, "Error on %s"%(execval))
result = False
- del self_config
+ self_config = None
return result
def load(self, file = None, part_filename="", tobytes=True, errorlog=True):
""" Load object with the file specified """
+ filename = ""
try:
filename = self.get_pathname(strings.tofilename(part_filename))
file, filename = self.open(file=file, read_write="r", part_filename=part_filename)
@@ -267,7 +293,7 @@
""" Convert the configuration to string """
data = self.__dict__.copy()
del data["modification_date"]
- return json.dumps(strings.tostrings(data))
+ return json.dumps(strings.tostrings(data),separators=(',', ':'))
def get_pathname(self, part_filename=""):
""" Get the configuration filename according to the class name """
@@ -307,6 +333,7 @@
Classes
def open(self, file=None, read_write="r", part_filename=""):
""" Create or open configuration file """
+ # pylint:disable=unspecified-encoding
filename = file
if filesystem.exists(self.config_root()) is False:
filesystem.makedir(self.config_root())
@@ -317,7 +344,7 @@
Classes
file = open(filename, read_write)
return file, filename
- def update(self, params):
+ def update(self, params, show_error=True):
""" Update object with html request params """
global self_config
if b"name" in params and b"value" in params and len(params) == 2:
@@ -326,6 +353,7 @@
Classes
else:
setmany = True
self_config = self
+
for name in self.__dict__.keys():
# Case of web input is missing when bool is false
if type(self.__dict__[name]) == type(True):
@@ -349,13 +377,32 @@
Classes
if setmany:
params[name] = False
# Case of web input is integer but string with number received
- elif type(self.__dict__[name]) == type(0) or type(self.__dict__[name]) == type(0.):
+ elif type(self.__dict__[name]) == type(0):
name = strings.tobytes(name)
if name in params:
try:
params[name] = int(params[name])
+ except:
+ if b"date" in name:
+ try:
+ params[name] = date.html_to_date(params[name])
+ except:
+ params[name] = 0
+ elif b"time" in name:
+ try:
+ params[name] = date.html_to_time(params[name])
+ except:
+ params[name] = 0
+ else:
+ params[name] = 0
+ elif type(self.__dict__[name]) == type(0.):
+ name = strings.tobytes(name)
+ if name in params:
+ try:
+ params[name] = float(params[name])
except:
params[name] = 0
+
result = True
for name, value in params.items():
execval = strings.tostrings(name)
@@ -364,7 +411,9 @@
Classes
# pylint: disable=exec-used
exec("a = self_config.%s"%execval)
existing = True
- except:
+ except Exception as err:
+ if "'NoneType' object" in str(err):
+ result = None
existing = False
if existing:
@@ -372,16 +421,17 @@
Classes
# pylint: disable=exec-used
exec(execval)
else:
- if name != b"action":
+ if name != b"action" and show_error and result is not None:
print("%s.%s not existing"%(self.__class__.__name__, strings.tostrings(name)))
except Exception as err:
logger.syslog(err, "Error on %s"%(execval))
result = False
- del self_config
+ self_config = None
return result
def load(self, file = None, part_filename="", tobytes=True, errorlog=True):
""" Load object with the file specified """
+ filename = ""
try:
filename = self.get_pathname(strings.tofilename(part_filename))
file, filename = self.open(file=file, read_write="r", part_filename=part_filename)
@@ -555,6 +605,7 @@
def update(self, params, show_error=True):
""" Update object with html request params """
global self_config
if b"name" in params and b"value" in params and len(params) == 2:
@@ -658,6 +710,7 @@
Methods
else:
setmany = True
self_config = self
+
for name in self.__dict__.keys():
# Case of web input is missing when bool is false
if type(self.__dict__[name]) == type(True):
@@ -681,13 +734,32 @@
Methods
if setmany:
params[name] = False
# Case of web input is integer but string with number received
- elif type(self.__dict__[name]) == type(0) or type(self.__dict__[name]) == type(0.):
+ elif type(self.__dict__[name]) == type(0):
name = strings.tobytes(name)
if name in params:
try:
params[name] = int(params[name])
+ except:
+ if b"date" in name:
+ try:
+ params[name] = date.html_to_date(params[name])
+ except:
+ params[name] = 0
+ elif b"time" in name:
+ try:
+ params[name] = date.html_to_time(params[name])
+ except:
+ params[name] = 0
+ else:
+ params[name] = 0
+ elif type(self.__dict__[name]) == type(0.):
+ name = strings.tobytes(name)
+ if name in params:
+ try:
+ params[name] = float(params[name])
except:
params[name] = 0
+
result = True
for name, value in params.items():
execval = strings.tostrings(name)
@@ -696,7 +768,9 @@
Methods
# pylint: disable=exec-used
exec("a = self_config.%s"%execval)
existing = True
- except:
+ except Exception as err:
+ if "'NoneType' object" in str(err):
+ result = None
existing = False
if existing:
@@ -704,12 +778,12 @@
Methods
# pylint: disable=exec-used
exec(execval)
else:
- if name != b"action":
+ if name != b"action" and show_error and result is not None:
print("%s.%s not existing"%(self.__class__.__name__, strings.tostrings(name)))
except Exception as err:
logger.syslog(err, "Error on %s"%(execval))
result = False
- del self_config
+ self_config = None
return result
pushover_token =b"API Jeton"
enter_pushover_token =b"Entrer le jeton API de pushover"
see_pushover_website =b"Voir le site web pushover"
-historic_not_available =b"Pas encore disponible, attendre 5 minutes"
+historic_not_available =b"Pas encore disponible, ressayez plus tard"
last_motion_detections =b"Derni\xC3\xA8res d\xC3\xA9tections de mouvement"
convert_ip_address =b"Convertir les adresses ip en noms DNS"
smartphone_d =b"Smartphone %d"
@@ -133,7 +133,7 @@
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Strings utilities """
import binascii
import time
-def local_time(date=None):
- """ Safe local time, it return 2000/1/1 00:00:00 if date can be extracted """
- try:
- year,month,day,hour,minute,second,weekday,yearday = time.localtime(date)[:8]
- except:
- year,month,day,hour,minute,second,weekday,yearday = 2000,1,1,0,0,0,0,0
- return year,month,day,hour,minute,second,weekday,yearday
-
-def date_to_string(date = None):
- """ Get a string with the current date """
- return date_to_bytes(date).decode("utf8")
-
-def date_to_bytes(date = None):
- """ Get a bytes with the current date """
- year,month,day,hour,minute,second,weekday,yearday = local_time(date)[:8]
- return b"%04d/%02d/%02d %02d:%02d:%02d"%(year,month,day,hour,minute,second)
-
-def date_ms_to_string():
- """ Get a string with the current date with ms """
- ms = (time.time_ns() // 1000000)%1000
- year,month,day,hour,minute,second,weekday,yearday = local_time(None)[:8]
- return "%04d/%02d/%02d %02d:%02d:%02d.%03d"%(year,month,day,hour,minute,second,ms)
-
-def date_to_filename(date = None):
- """ Get a filename with a date """
- filename = date_to_string(date)
- filename = filename.replace(" "," ")
- filename = filename.replace(" ","_")
- filename = filename.replace("/","-")
- filename = filename.replace(":","-")
- return filename
-
-def date_to_path(date=None):
- """ Get a path with year/month/day/hour """
- year,month,day,hour,minute,second,weekday,yearday = local_time(date)[:8]
- return b"%04d/%02d/%02d/%02dh%02d"%(year,month,day,hour,minute)
def size_to_string(size, largeur=6):
""" Convert a size in a string with k, m, g, t..."""
@@ -378,85 +343,6 @@
Functions
return hash_ % 65536
-
-def date_ms_to_string()
-
-
-
Get a string with the current date with ms
-
-
-Expand source code
-
-
def date_ms_to_string():
- """ Get a string with the current date with ms """
- ms = (time.time_ns() // 1000000)%1000
- year,month,day,hour,minute,second,weekday,yearday = local_time(None)[:8]
- return "%04d/%02d/%02d %02d:%02d:%02d.%03d"%(year,month,day,hour,minute,second,ms)
-
-
-
-def date_to_bytes(date=None)
-
-
-
Get a bytes with the current date
-
-
-Expand source code
-
-
def date_to_bytes(date = None):
- """ Get a bytes with the current date """
- year,month,day,hour,minute,second,weekday,yearday = local_time(date)[:8]
- return b"%04d/%02d/%02d %02d:%02d:%02d"%(year,month,day,hour,minute,second)
-
-
-
-def date_to_filename(date=None)
-
-
-
Get a filename with a date
-
-
-Expand source code
-
-
def date_to_filename(date = None):
- """ Get a filename with a date """
- filename = date_to_string(date)
- filename = filename.replace(" "," ")
- filename = filename.replace(" ","_")
- filename = filename.replace("/","-")
- filename = filename.replace(":","-")
- return filename
-
-
-
-def date_to_path(date=None)
-
-
-
Get a path with year/month/day/hour
-
-
-Expand source code
-
-
def date_to_path(date=None):
- """ Get a path with year/month/day/hour """
- year,month,day,hour,minute,second,weekday,yearday = local_time(date)[:8]
- return b"%04d/%02d/%02d/%02dh%02d"%(year,month,day,hour,minute)
-
-
-
-def date_to_string(date=None)
-
-
-
Get a string with the current date
-
-
-Expand source code
-
-
def date_to_string(date = None):
- """ Get a string with the current date """
- return date_to_bytes(date).decode("utf8")
-
-
def dump(buff, withColor=True)
@@ -754,24 +640,6 @@
Functions
return False
-
-def local_time(date=None)
-
-
-
Safe local time, it return 2000/1/1 00:00:00 if date can be extracted
-
-
-Expand source code
-
-
def local_time(date=None):
- """ Safe local time, it return 2000/1/1 00:00:00 if date can be extracted """
- try:
- year,month,day,hour,minute,second,weekday,yearday = time.localtime(date)[:8]
- except:
- year,month,day,hour,minute,second,weekday,yearday = 2000,1,1,0,0,0,0,0
- return year,month,day,hour,minute,second,weekday,yearday
List the supported configuration according to the hardware
+
+
+Expand source code
+
+
""" List the supported configuration according to the hardware """
+
+import sys
+
+def telnet():
+ """ Indicates if telnet is supported by this hardware """
+ if sys.platform == "rp2":
+ return False
+ return True
+
+def hostname():
+ """ Indicates if hostname can be used by this hardware """
+ if sys.platform == "rp2":
+ return False
+ return True
+
+def static_ip_accesspoint():
+ """ Indicates if static ip on access point can be used by this hardware """
+ if sys.platform == "rp2":
+ return False
+ return True
+
+def battery():
+ """ Indicates if battery setup supported """
+ if sys.platform == "rp2":
+ return False
+ return True
Indicates if hostname can be used by this hardware
+
+
+Expand source code
+
+
def hostname():
+ """ Indicates if hostname can be used by this hardware """
+ if sys.platform == "rp2":
+ return False
+ return True
+
+
+
+def static_ip_accesspoint()
+
+
+
Indicates if static ip on access point can be used by this hardware
+
+
+Expand source code
+
+
def static_ip_accesspoint():
+ """ Indicates if static ip on access point can be used by this hardware """
+ if sys.platform == "rp2":
+ return False
+ return True
+
+
+
+def telnet()
+
+
+
Indicates if telnet is supported by this hardware
+
+
+Expand source code
+
+
def telnet():
+ """ Indicates if telnet is supported by this hardware """
+ if sys.platform == "rp2":
+ return False
+ return True
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/doc/lib/tools/system.html b/doc/lib/tools/system.html
index 719ef7b..c756845 100644
--- a/doc/lib/tools/system.html
+++ b/doc/lib/tools/system.html
@@ -36,8 +36,8 @@
Module lib.tools.system
def reboot(message="Reboot"):
""" Reboot command """
logger.syslog(message)
- from tools import lang
- region_config = lang.RegionConfig()
+ from tools import region
+ region_config = region.RegionConfig()
if region_config.load():
region_config.current_time = time.time() + 8
region_config.save()
@@ -73,8 +73,8 @@
Functions
def reboot(message="Reboot"):
""" Reboot command """
logger.syslog(message)
- from tools import lang
- region_config = lang.RegionConfig()
+ from tools import region
+ region_config = region.RegionConfig()
if region_config.load():
region_config.current_time = time.time() + 8
region_config.save()
diff --git a/doc/lib/tools/tasking.html b/doc/lib/tools/tasking.html
index 98928c5..686d288 100644
--- a/doc/lib/tools/tasking.html
+++ b/doc/lib/tools/tasking.html
@@ -29,9 +29,10 @@
Module lib.tools.tasking
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Miscellaneous utility functions """
import machine
-from tools import strings,logger,system,watchdog
+from tools import lang,strings,logger,system,watchdog, info
class Inactivity:
""" Class to manage inactivity timer """
@@ -60,7 +61,7 @@
Module lib.tools.tasking
self.stop()
self.start()
-async def task_monitoring(task):
+async def task_monitoring(task, *args, **params):
""" Check if task crash, log message and reboot if it too frequent """
import uasyncio
retry = 0
@@ -71,10 +72,10 @@
Module lib.tools.tasking
while retry < max_retry:
try:
while True:
- if await task():
+ if await task(*args, **params):
retry = 0
except MemoryError as err:
- lastError = logger.syslog(err, "Memory error")
+ lastError = logger.syslog(err, "Memory error, %s"%strings.tostrings(info.meminfo()))
from gc import collect
collect()
memory_error_count += 1
@@ -95,7 +96,6 @@
Module lib.tools.tasking
config = ServerConfig()
config.load()
- from tools import lang
await Notifier.notify(lang.reboot_after_many%strings.tobytes(lastError), enabled=config.notify)
finally:
system.reboot()
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Class to manage the camera of the ESP32CAM.
This requires the modified firmware.
I added in the firmware the possibility of detecting movements,
diff --git a/doc/lib/webpage/awakepage.html b/doc/lib/webpage/awakepage.html
index 106c3b5..24589fd 100644
--- a/doc/lib/webpage/awakepage.html
+++ b/doc/lib/webpage/awakepage.html
@@ -42,12 +42,13 @@
def main_frame(request, response, args, title_frame, *content):
""" Function define the main frame into the main page with menu, it check also the login password """
- internal = [Title3(text=titleFrame, style=b"padding-top:0.5em;padding-bottom:0.5em;"),content]
- return main_page(request, response, args, content=internal)
if new_password is not None:
res = User.change(request.params.get(b"user", b""), request.params.get(b"current_password", b""), request.params.get(b"new_password"), request.params.get(b"renew_password"))
if res is True:
- return [Br(),AlertSuccess(text=lang.password_changed)]
+ return [AlertSuccess(text=lang.password_changed)]
elif res is None:
return PasswordPage.change_page(alert=lang.passwords_not_equals)
else:
@@ -81,8 +81,8 @@
if new_password is not None:
res = User.change(request.params.get(b"user", b""), request.params.get(b"current_password", b""), request.params.get(b"new_password"), request.params.get(b"renew_password"))
if res is True:
- return [Br(),AlertSuccess(text=lang.password_changed)]
+ return [AlertSuccess(text=lang.password_changed)]
elif res is None:
return PasswordPage.change_page(alert=lang.passwords_not_equals)
else:
@@ -167,8 +167,8 @@
if new_password is not None:
res = User.change(request.params.get(b"user", b""), request.params.get(b"current_password", b""), request.params.get(b"new_password"), request.params.get(b"renew_password"))
if res is True:
- return [Br(),AlertSuccess(text=lang.password_changed)]
+ return [AlertSuccess(text=lang.password_changed)]
elif res is None:
return PasswordPage.change_page(alert=lang.passwords_not_equals)
else:
@@ -258,8 +258,8 @@
from server.httpserver import HttpServer
from htmltemplate import *
from webpage.mainpage import main_frame, manage_default_button
-from tools import lang
+from tools import lang, region
@HttpServer.add_route(b'/region', menu=lang.menu_account, item=lang.item_region)
async def region_page(request, response, args):
""" Function define the web page to manage lang and time """
- config = lang.RegionConfig()
+ config = region.RegionConfig()
disabled, action, submit = manage_default_button(request, config)
langages = []
for langage in [b"english",b"french"]:
@@ -49,14 +49,17 @@
@HttpServer.add_route(b'/region', menu=lang.menu_account, item=lang.item_region)
async def region_page(request, response, args):
""" Function define the web page to manage lang and time """
- config = lang.RegionConfig()
+ config = region.RegionConfig()
disabled, action, submit = manage_default_button(request, config)
langages = []
for langage in [b"english",b"french"]:
@@ -90,14 +93,17 @@
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
""" Classes used to manage the wifi access point """
+import sys
from wifi import hostname, ip
from tools import jsonconfig,strings,logger
@@ -71,22 +73,28 @@
Module lib.wifi.accesspoint
@staticmethod
def open(ssid=None, password=None, authmode=None):
""" Open access point """
- from wifi import AUTHMODE
+ from wifi import AUTHMODE, AUTHMODE_DEFAULT
# pylint:disable=multiple-statements
if ssid is not None: AccessPoint.config.ssid = strings.tobytes(ssid)
if password is not None: AccessPoint.config.wifi_password = strings.tobytes(password)
if authmode is not None: AccessPoint.config.authmode = strings.tobytes(authmode)
- authmode = 3
+ authmode = AUTHMODE_DEFAULT
for authmode_num, authmode_name in AUTHMODE.items():
if authmode_name == AccessPoint.config.authmode:
authmode = authmode_num
break
- AccessPoint.wlan.active(True) # IMPORTANT : Activate before configure
+
+ if sys.platform != "rp2":
+ AccessPoint.wlan.active(True) # IMPORTANT : For esp32 activate before configure
+
AccessPoint.wlan.config(\
essid = strings.tostrings(AccessPoint.config.ssid),
password = strings.tostrings(AccessPoint.config.wifi_password),
- authmode = authmode)
+ security = authmode)
+
+ if sys.platform == "rp2":
+ AccessPoint.wlan.active(True) # IMPORTANT : For esp32 activate before configure
@staticmethod
def reload_config():
@@ -152,11 +160,12 @@
@staticmethod
def open(ssid=None, password=None, authmode=None):
""" Open access point """
- from wifi import AUTHMODE
+ from wifi import AUTHMODE, AUTHMODE_DEFAULT
# pylint:disable=multiple-statements
if ssid is not None: AccessPoint.config.ssid = strings.tobytes(ssid)
if password is not None: AccessPoint.config.wifi_password = strings.tobytes(password)
if authmode is not None: AccessPoint.config.authmode = strings.tobytes(authmode)
- authmode = 3
+ authmode = AUTHMODE_DEFAULT
for authmode_num, authmode_name in AUTHMODE.items():
if authmode_name == AccessPoint.config.authmode:
authmode = authmode_num
break
- AccessPoint.wlan.active(True) # IMPORTANT : Activate before configure
+
+ if sys.platform != "rp2":
+ AccessPoint.wlan.active(True) # IMPORTANT : For esp32 activate before configure
+
AccessPoint.wlan.config(\
essid = strings.tostrings(AccessPoint.config.ssid),
password = strings.tostrings(AccessPoint.config.wifi_password),
- authmode = authmode)
+ security = authmode)
+
+ if sys.platform == "rp2":
+ AccessPoint.wlan.active(True) # IMPORTANT : For esp32 activate before configure
# Distributed under MIT License
# Copyright (c) 2021 Remi BERTHOLET
+# pylint:disable=consider-using-f-string
+
""" Manages access to wifi, treats cases of network loss with retry, and manages a fallback on the access point if no network is available """
from wifi.accesspoint import *
from wifi.station import *
@@ -107,7 +109,7 @@
Module lib.wifi.wifi
""" Indicates that wan have probably a problem """
if Wifi.get_state() in [WIFI_CONNECTED, LAN_CONNECTED, WAN_CONNECTED ]:
Wifi.context.wan_problem += 1
- logger.syslog("WAN problem %d detected (max=%d)"%(Wifi.context.wan_problem,MAX_PROBLEM))
+ logger.syslog("WAN problem %d detected (max=%d) (wifi=%s)"%(Wifi.context.wan_problem,MAX_PROBLEM, strings.tostrings(Station.get_signal_strength_bytes())))
@staticmethod
def lan_connected(changeState=True):
@@ -124,7 +126,7 @@
Module lib.wifi.wifi
""" Indicates that lan disconnection detected """
if Wifi.get_state() in [WIFI_CONNECTED, LAN_CONNECTED, WAN_CONNECTED ]:
Wifi.context.lan_problem += 1
- logger.syslog("LAN problem %d detected (max=%d)"%(Wifi.context.lan_problem, MAX_PROBLEM))
+ logger.syslog("LAN problem %d detected (max=%d)(wifi=%s)"%(Wifi.context.lan_problem, MAX_PROBLEM, strings.tostrings(Station.get_signal_strength_bytes())))
@staticmethod
def is_wan_available():
@@ -155,7 +157,7 @@
Module lib.wifi.wifi
# If the wifi not started
if state == WIFI_OFF:
Wifi.context.dns = ""
-
+
# If wifi station available
if Station.is_activated():
AccessPoint.stop()
@@ -209,7 +211,7 @@
Module lib.wifi.wifi
Wifi.context.dns = Station.get_info()[3]
Wifi.context.lan_problem = 0
Wifi.context.wan_problem = 0
-
+
# If station yet activated
if Station.is_activated():
# If too many problem detected
@@ -300,7 +302,7 @@
Classes
""" Indicates that wan have probably a problem """
if Wifi.get_state() in [WIFI_CONNECTED, LAN_CONNECTED, WAN_CONNECTED ]:
Wifi.context.wan_problem += 1
- logger.syslog("WAN problem %d detected (max=%d)"%(Wifi.context.wan_problem,MAX_PROBLEM))
+ logger.syslog("WAN problem %d detected (max=%d) (wifi=%s)"%(Wifi.context.wan_problem,MAX_PROBLEM, strings.tostrings(Station.get_signal_strength_bytes())))
@staticmethod
def lan_connected(changeState=True):
@@ -317,7 +319,7 @@
Classes
""" Indicates that lan disconnection detected """
if Wifi.get_state() in [WIFI_CONNECTED, LAN_CONNECTED, WAN_CONNECTED ]:
Wifi.context.lan_problem += 1
- logger.syslog("LAN problem %d detected (max=%d)"%(Wifi.context.lan_problem, MAX_PROBLEM))
+ logger.syslog("LAN problem %d detected (max=%d)(wifi=%s)"%(Wifi.context.lan_problem, MAX_PROBLEM, strings.tostrings(Station.get_signal_strength_bytes())))
@staticmethod
def is_wan_available():
@@ -348,7 +350,7 @@
Classes
# If the wifi not started
if state == WIFI_OFF:
Wifi.context.dns = ""
-
+
# If wifi station available
if Station.is_activated():
AccessPoint.stop()
@@ -402,7 +404,7 @@
Classes
Wifi.context.dns = Station.get_info()[3]
Wifi.context.lan_problem = 0
Wifi.context.wan_problem = 0
-
+
# If station yet activated
if Station.is_activated():
# If too many problem detected
@@ -563,7 +565,7 @@
Static methods
""" Indicates that lan disconnection detected """
if Wifi.get_state() in [WIFI_CONNECTED, LAN_CONNECTED, WAN_CONNECTED ]:
Wifi.context.lan_problem += 1
- logger.syslog("LAN problem %d detected (max=%d)"%(Wifi.context.lan_problem, MAX_PROBLEM))
+ logger.syslog("LAN problem %d detected (max=%d)(wifi=%s)"%(Wifi.context.lan_problem, MAX_PROBLEM, strings.tostrings(Station.get_signal_strength_bytes())))
@@ -584,7 +586,7 @@
Static methods
# If the wifi not started
if state == WIFI_OFF:
Wifi.context.dns = ""
-
+
# If wifi station available
if Station.is_activated():
AccessPoint.stop()
@@ -638,7 +640,7 @@
Static methods
Wifi.context.dns = Station.get_info()[3]
Wifi.context.lan_problem = 0
Wifi.context.wan_problem = 0
-
+
# If station yet activated
if Station.is_activated():
# If too many problem detected
@@ -717,7 +719,7 @@
Static methods
""" Indicates that wan have probably a problem """
if Wifi.get_state() in [WIFI_CONNECTED, LAN_CONNECTED, WAN_CONNECTED ]:
Wifi.context.wan_problem += 1
- logger.syslog("WAN problem %d detected (max=%d)"%(Wifi.context.wan_problem,MAX_PROBLEM))
+ logger.syslog("WAN problem %d detected (max=%d) (wifi=%s)"%(Wifi.context.wan_problem,MAX_PROBLEM, strings.tostrings(Station.get_signal_strength_bytes())))
diff --git a/images/Device_BPI-Leaf-S3.png b/images/Device_BPI-Leaf-S3.png
new file mode 100644
index 0000000..be5d6db
Binary files /dev/null and b/images/Device_BPI-Leaf-S3.png differ
diff --git a/images/ResponsiveWebInterface.gif b/images/ResponsiveWebInterface.gif
new file mode 100644
index 0000000..d6346c6
Binary files /dev/null and b/images/ResponsiveWebInterface.gif differ
diff --git a/modules/lib/tools/builddate.py b/modules/lib/tools/builddate.py
index c6f11d0..23bb230 100644
--- a/modules/lib/tools/builddate.py
+++ b/modules/lib/tools/builddate.py
@@ -1,2 +1,2 @@
''' Build date '''
-date=b'2022/12/28 09:46:34'
+date=b'2023/01/22 16:34:03'
diff --git a/modules/main.py b/modules/main.py
index 461c6ad..976e275 100644
--- a/modules/main.py
+++ b/modules/main.py
@@ -22,34 +22,12 @@
# Create camera and motion detection asynchronous task
pycameresp.create_camera_task(loop, device)
-# from server import openmeteo
-
-# Sample of user task
-# async def meteo_task():
-# """ Get meteo from openmeteo """
-# while True:
-# await openmeteo.async_get_meteo(b"?latitude=52.52&longitude=13.41&hourly=temperature_2m,relativehumidity_2m,windspeed_10m")
-# await uasyncio.sleep(1)
-
-# Register the user task, monitor all exceptions
-# pycameresp.create_user_task(loop, meteo_task)
-
def sample_html_page_loader():
""" Html page loader. Html pages are loaded in memory only when the web server is used """
try :
import sample
except:
pass
- try:
- import electricmeter
- except:
- pass
-
-try:
- from electricmeter import create_electric_meter
- create_electric_meter(loop, gpio=21)
-except Exception as err:
- print(err)
# Create servers, network tools, wifi manager asynchronous task
pycameresp.create_network_task(loop, sample_html_page_loader)
diff --git a/tools/camflasher/main.py b/tools/camflasher/main.py
index 97cec0e..582d57f 100755
--- a/tools/camflasher/main.py
+++ b/tools/camflasher/main.py
@@ -69,6 +69,8 @@ def update(self, detected_ports):
if self.status[key] is False:
self.status[key] = True
result = True
+ else:
+ pass
else:
# Create new port
self.status[key] = True
@@ -76,6 +78,8 @@ def update(self, detected_ports):
self.config.setValue(settings.DEVICE_RTS_DTR,self.rts_dtr)
connected.append(detected_port.device)
result = True
+ else:
+ pass
# For all ports already registered
for key in self.status:
@@ -83,13 +87,18 @@ def update(self, detected_ports):
for detected_port in sorted(detected_ports):
# If port is yet connected
if key[0] == detected_port.device and key[1] == detected_port.vid and key[2] == detected_port.pid:
+ result = True
break
+ else:
+ pass
else:
# The port is disconnected
self.status[key] = False
result = True
if result is True:
return connected
+ else:
+ pass
return None
def get_rts_dtr(self, name):