3. August 2023

 

HomeAssistant - after aprrox. 2 years usage

In an older post I found that I started home automation with OpenHAB.

A few days later I switched to HomeAssistant and I'm still using it.

Getting it up and running on a Raspi 4 was quite easy.

However, the complexity of integrating devices differs wildly.

My setup is now like this:



1. November 2021

 

Design flaws in Oracle PL/SQL and ideas how to fix them

Thinking about Oracle PL/SQL and SQL flaws and how they could be fixed

Background

First of all, let me say that I like Oracle PL/SQL.

I've been working with it since 1994, and it is remarkable that a programming language lasts such a long time.

Jist think of other languages that have come and gone in these decades.

In our company, there have been some constants more or less since I started in that year, and these were:

A main reason why our company does not pick up new trends frequently is the kind of software that we are developing (a LIMS) and the kind of our customers (pharmaceutics, industry). The software has an impact to the way our customers are working comparable to e.g. SAP. Thus it intended to run for at least a decade when installed. Of course this doesn't mean there are no updates, but it means that we need a very reliable framework.

So most of the business logic was and still is implemented in PL/SQL, while the technologies for UI and reporting changed over time.

Reasons why I like PL/SQL

It is remarkable that AFAIK there have never been severe (e.g. security) problems with PL/SQL in all this time. This may be related to the fact that PL/SQL is somewhat "old-fashioned".

It's tight integration with SQL makes developing business logic for (Oracle) database applications very easy.

It supports NULL so in this regard it matches SQL exactly.

It is easy to learn and easy to read.

It is very stable and solid - I mean the running programs as well as its development over the decades.

What is not so good (but cannot be changed)

PL/SQL is quite verbose.

String processing is often cumbersome.

It counts 1-based and not 0-based (this matches SQL and saying this not so good is just my personal opinion).

The design flaws

From my experience, the following are serious design flaws which often cause hard-to-find errors.

Implicit type conversion

There are implicit type conversions everywhere. I think this is the number one design flaw in most programming languages, but in SQL and PL/SQL it is worse because in these languages the conversions are NLS dependent by default - and the languages have been designed by Americans who probably were not aware of this topic.

In the following examples, the variable names reflect their type:

v_date := v_str; -- IMPLICIT: TO_DATE without a format string
v_str := v_date; -- IMPLICIT: TO_CHAR w/o format
v_num := v_str;  -- IMPLICIT: TO_NUMBER w/o format
v_str := v_num;  -- IMPLICIT: TO_CHAR w/o format

v_str + v_num; -- IMPLICIT: TO_NUMBER w/o format
v_dat + v_str; -- IMPLICIT: TO_NUMBER w/o format
v_str || v_num; -- IMPLICIT: TO_CHAR w/o format
v_str || v_dat; -- IMPLICIT: TO_CHAR w/o format

The implicit conversion between various number data types can cause trouble as well.
To reduce errors, it should not be possible to mix them in assignment or calculations, because they can cause a precision-loss or rounding errors or conversion errors (when converting between internal 10-based storage and 2-based storage.

For conversion between strings and numbers: The results (or occurence of exceptions) depend on the NLS_NUMERIC_CHARACTERS. Furthermore, in Germany we usually write a leading zero for values between 0 and 1, e.g. '0,25' instead of '0.25' for a quarter.

On TO_NUMBER, the wrong decimal and group character can cause immediate exceptions or (much worse!) a result can be mis-interpreted, e.g. 1.234 means 1234 in German (with point as a thousands separator) but 1234/1000 in US English.
On TO_CHAR, there will be an exception, but mis-interpretation (by a human reader or another program when it tries to interpete the string as a number later) can still occur.
 
On TO_DATE, the result depends on the NLS_DATE_FORMAT setting. E.g. in some countries, the default may be MM-DD-YY while in Germany DD.MM.YYYY is common. So this can result in an exception or in a misinterpretation just like with numbers.

Each and every implicit conversion should be reported as a PL/SQL warning.

Each and every TO_NUMBER and TO_DATE without a format should be reported as a PL/SQL warning.

BTW if there was no implicit conversion, then the + operator could be used for strings in a safe manner, we would not need the || operator.

All string types can be assigned despite the maximum length

This is a special case of implicit type conversion.

Say, you have a source string v_source which is from a VARCHAR2(1000) column and a destination variable v_dest declared as VARCHAR2(3). You can assign 
v_dest := v_source and if the actual length is >3 you'll get a VALUE_ERROR exception.

Strings with different maximum lengths shouldn't be assignable without explicit conversion if the source can be longer than the destination or if at least one of it has unknown length.
And for the explicit conversion, one should be able to tell the compiler if a too-long input value should result in a VALUE_ERROR or  be truncated silently. Maybe two different functions should be introduced or an optional parameter silent_truncate.

Maximum length and nullability of parameters and return type cannot be declared or accessed

This is related to the previous problem.
You cannot declare a parameter as VARCHAR2(30), only as VARCHAR2 with unknown length. And even if your parameter is declared as EMP.ENAME%TYPE (where the maximum length is known from the column ENAME of the EMP table), you cannot access the length inside the function.

This is a problem in particular for output parameters and return values. E.g. you generate some error message or comment for an output parameter or the return value and you want to give as much information as possible, but you have to avoid a VALUE_ERROR.

For return values, another effect of this problem is that a column which is the result of a VARCHAR2 function always has a length of 4000.

Wouldn't it be possible to have attributes for columns, variables and parameters like the following?

xxx%MAX_CHARS returns the maximum number of characters of xxx or NULL if the maximum length is unknown.

xxx%NULLABLE returns 1 if the compiler knows that xxx may contain nulls, 0 if the compiler knows that xxx must be NOT NULL and NULL if the compiler does not know.

Note that I do not mean the compiler that e.g. an input variable declared as a PK column %TYPE must be NOT NULL. This would change existing behavior too dramatically.

Actually checking that all input parameter values declared as NOT NULL really are NOT NULL on input and that all NOT NULL output columns and the return value are NOT NULL on output (exception when an exception occured) could be done with a PRAGMA CHECK_NOT_NULL.

Calling procedures/functions with a lot of parameters without =>

A quite common cause for subtle bugs is to define procedures or functions with a lot of parameters.
This is often the easiest way to achieve a goal, but if you have a function with more than a few parameters, it should be an error to call it with positional assignment without explicitly assigning every (used) parameters with the pi_name => v_value syntax.
It is hard to define a fixed value for the maximum allowed number of parameters for positional assignment, but I think a maximum of 5 is ok.
Dear Oracle: This could be easily assed as a PL/SQL warning.

For more than 5 arguments, the risk of a wrong ordering of the arguments is too high.

Lack of a type registry

What I mean here is best shown with a simple example. Just think of the SCOTT/TIGER example schema with its DEPT and EMP tables.
The DEPTNO is declared as NUMBER(4) for example. In the EMP table, we also have a DEPTNO column referencing the DEPT table. We have to duplicate the NUMBER(4) here. If we later have a really big company with longer DEPNTO values - e.g. NUMBER(6), then we have to remember to not only change the column in the DEPT table, but also in the EMP table. Okay, in this special case one could argue that it is easy because the column name is the same, but things are not always so easy.
If we had one or more schema-specific type registry (for example given in a package with a special name or pragma and without a body), we could have a type DEPTNO_TYPE and use that in the CREATE TABLE statements for the DEPT and the EMP tables instead of duplication NUMBER(4).
This does not work unfortunately, because definitions of table column types cannot use  packages. But I see no reason why this rule couldn't be relaxed.

Like this:
create or replace package MY_TYPES
is
    PRAGMA PROVIDES_COLUMN_TYPES;
    DEPTNO_TYPE NUMBER(4);
    EMPNO_TYPE NUMBER(5);
    ...
end MY_TYPES;
/
create table DEPT
( DEPTNO MY_TYPES.DEPTNO_TYPE NOT NULL
, ...
);
create table EMP
( EMPNO MY_TYPES.EMPNO_TYPE NOT NULL
, ENAME varchar2(30)
, DEPTNO MY_TYPES.DEPTNO_TYPE
, ...
);

Note that actually implementing OR REPLACE will be difficult, in particular if one decides to shorten a type. This could result in ALTER TABLE MODIFY etc. being necessary. 

Exception handling

The exception handling mechanisms were probably excellent back in the old days when the language was introduced, but today - well, I see some things that could be a lot better.

First of all, the exceptions are unstructured. They have unique numbers, which is good, but there is no kind of hierarchy. For example, you cannot easily decide between problems with the data (such as TOO_MANY_ROWS, NO_DATA_FOUND, DUP_VAL_ON_INDEX) or problems with the database itself such as corrupt blocks or whatever.

Aonther problem with exceptions is that there is no equivalent to the finally clause of other programming languages. You need this to free resources in many cases. The only thing you can do is something like
EXCEPTION
  WHEN OTHERS THEN
    CLEAN_UP();
    RAISE;
END;
This works, but the RAISE loses information about the location where the exception originally occured.

And a third problem - at least for bigger systems like ours (with hundreds of tables and packages) is the limited way of  defining your own exceptions. With RAISE_APPLICATION_ERROR you only have a global maximum of 1000 different error codes. This means that the error code alone is not sufficient for example to translate error messages. It would help a lot if one could add different custom error prefixes instead of the auto-generated ORA prefix. That way the unique key for an error would be the tuple(PREFIX, SQLCODE) instead of just SQLCODE.  

A forth problem is that you cannot add additional properties to an exception: Just the SQLCODE and the text in SQLERRM. It would be cool if you could for example add some information from the application domain in some kind of key->value dictionary.

Getting exception handling right is difficult in PL/SQL - just like in many other programming languages. The best exception handling is nothing at all, except handling exactly only the expected exceptions like TOO_MANY_ROWS, NO_DATA_FOUND or DUP_VAL_ON_INDEX. And to use WHEN OTHERS only if really needed to clean up resources, followed by a RAISE (but as mentioned before, this looses information about the exception's origin).

Other issues

CAST does not allow %TYPE

Sometimes you want to use %TYPE more or less just as a SUBSTR to truncate a string to the maximum allowed length for a given column. Can't remember an example now, but I had this problem several times.

A similar problem was when declaring types (at schema level, not inside a package).

The prefix FX for date conversion is not the default

For those who do not know (which means: most PL/SQL developers in my experience), the TO_DATE conversion can give unexpected results if the input string is shorter than expected. The FX prefix means that the input string must match the format pattern exactly.  

Labels: , , ,


16. Oktober 2020

 

OpenHAB2 - Mühsam ernährt sich das Eichhörnchen

Einige Notizen (keine vollständige Anleitung: bin selbst Anfänger) zu OpenHAB2, inkl. Backup

Die Einrichtung von OpenHAB2 nach der holprigen Installation ist erstmal nicht so schwierig, man bekommt es leicht ans Rennen. Aber:
  • Die Software bringt ihre eigene Nomenklatur mit: Bindings, Services, Things, Items, Channels. Wozu das alles gut ist, habe ich bisher noch nicht verstanden.
  • MQTT ist nicht ab Werk dabei und muss nachinstalliert werden
  • Die Konfiguration ist völlig wir. Konfigurationsdateien? Gibt es an zwei verschiedenen Orten. Außerdem noch Paper UI für die Konfiguration. Wo speichert das die Konfiguration?
  • Der Systemstart dauert extrem lange. 10 Minuten lang rödelt der Raspi auf 90% CPU, und das wo ich bisher nur eine Steckdose eingebunden habe...
Ich werde vermutlich auch Home Assistant ausprobieren. Das ist mir eh sympathischer, da Python-basiert.

Andererseits gefällt mir die Android APP von OpenHAB2, insb. die Möglichkeit, per Mikro (über Google Spracherkennung) Befehle an die Software zu übermitteln - auch wenn ich noch nicht weiß, was ich da noch konkret konfigurieren muss.

Ich habe bisher manuell folgende Konfigurationsdateien angelegt:

Zum Anlegen habe ich die Unterstützung des Sitemap Designers (oder so ähnlich) im Browser verwendet.

Datei /etc/openhab2/items/kronocken.items

Hier ist die bisher einzige Steckdose konfiguriert:

Group    Home                  "Kronocken"      <house>                                   ["Building"]

Group    GF                    "Erdgeschoss"    <groundfloor>   (Home)                    ["GroundFloor"]
Group    FF                    "Erster Stock"   <firstfloor>    (Home)                    ["FirstFloor"]
Group    C                     "Keller"         <cellar>        (Home)                    ["Basement"]
Group    OU                    "Draußen"        <garden>        (Home)                    ["Outdoor"]

Group    GF_LivingRoom         "Wohnzimmer"     <sofa>          (Home, GF)                ["LivingRoom"]
Group    GF_Kitchen            "Küche"          <kitchen>       (Home, GF)                ["Kitchen"]
Group    GF_Bathroom           "Badezimmer"     <bath>          (Home, GF)                ["Bathroom"]
Group    FF_Bedroom            "Schlafzimmer"   <bedroom>       (Home, FF)                ["Bedroom"]
Group    FF_KidsRoom           "Kinderzimmer"   <girl_3>        (Home, FF)                ["Room"]
Group    FF_Bathroom           "Badezimmer"     <bath>          (Home, FF)                ["Bathroom"]
Group    C_Basement            "Keller"         <cellar>        (Home, C)                 ["Basement"]
Group    OU_Driveway           "Auffahrt"                       (Home, OU)                ["Outdoor"]
Group    OU_Terrace            "Terrasse"       <terrace>       (Home, OU)                ["Terrace"]

Switch   GF_LivingRoom_Power   "Steckdose 1"      <poweroutlet>   (GF_LivingRoom, gPower)   ["Switch", "Switchable"]   {mqtt=">[mqtt_service:/cmnd/steckdose1:power:ON:1],>[mqtt_service:/cmnd/steckdose1:power:OFF:0]"}
Switch   FF_Bedroom_Power      "Steckdose 2"      <poweroutlet>   (FF_Bedroom, gPower)      ["Switch", "Switchable"]   {channel=""}

Group:Switch:OR(ON, OFF)   gPower   "Steckdose"   <poweroutlet>   (Home)   ["Switch", "Switchable"]

Datei /etc/openhab2/sitemaps/kronocken.sitemap

sitemap kronocken label="Kronocken" {
    Frame label="Erdgeschoss" icon="groundfloor" {
        Group item=GF_LivingRoom
        Group item=GF_Kitchen
        Group item=GF_Bathroom
    }

    Frame label="Erster Stock" icon="firstfloor" {
        Group item=FF_Bedroom
        Group item=FF_KidsRoom
        Group item=FF_Bathroom
    }

    Frame label="Keller" icon="cellar" {
        Group item=C_Basement
    }

    Frame label="Draußen" icon="garden" {
        Group item=OU_Driveway
        Group item=OU_Terrace
    }

    Frame {
        Text label="Steckdose" icon="poweroutlet" {
            Default item=GF_LivingRoom_Power label="Wohnzimmer"
            Default item=FF_Bedroom_Power label="Schlafzimmer"
        }
    }
}

Diverse andere Dateien...

... habe ich im Backup unter userdata/config/org/openhab gefunden. Das sind anscheinend die Dateien mit Konfigurationsdaten, die ich im"Paper UI" angelegt habe. Dazu gehört unter anderem die Datei mqtt.config mit dem Inhalt
:org.apache.felix.configadmin.revision:=L"1"
broker.pwd="openhabian"
broker.url="tcp://localhost:1883"
broker.user="openhabian"
service.pid="org.openhab.mqtt"
Sprich, hier ist der MQTT Broker konfiguriert, den ich über das MQTT Binding definiert habe.

Die Datei addons.config enthält die Liste der installierten Add-Ons.

Backup und Restore

Mit Hilfe von Skripten auf Windows- und Linux-Seite und ssh habe ich eine einfache Backup- und Restore-Möglichkeit gefunden (hoffe ich...).

Windows-Seite



Skript backup.cmd

@echo off
setlocal
for /F "usebackq tokens=1,2 delims==" %%i in (`wmic os get LocalDateTime /VALUE 2^>NUL`) do if '.%%i.'=='.LocalDateTime.' set ldt=%%j
set ldt=%ldt:~0,14%
ssh -i .ssh\id_rsa pi@192.168.178.21 -t "backup"
scp -i .ssh\id_rsa pi@192.168.178.21:/home/pi/openhab-backup.zip backups\backup-%ldt%.zip

set DATEIGROESSE=0
for %%I in (backups\backup-%ldt%.zip) do set DATEIGROESSE=%%~zI
if not exist backups\backup-%ldt%.zip echo Backup ist fehlgeschlagen - Backup-Datei fehlt && goto :fehler
if %DATEIGROESSE% EQU 0 echo Backup ist fehlgeschlagen - Backup-Datei ist leer. && goto :fehler
if %DATEIGROESSE% GTR 0 echo Backup erfolgreich, Dateigroesse %DATEIGROESSE%
endlocal
goto :EOF

:fehler
endlocal
exit /b 1

Skript restore.cmd

@echo off
setlocal
set backup_file=%1
if not defined backup_file echo Bitte eine Datei aus backups\ als Parameter beim Aufruf angeben && goto :fehler
if not exist %backup_file% echo Backup ist fehlgeschlagen - Backup-Datei fehlt && goto :fehler
scp -i .ssh\id_rsa %backup_file% pi@192.168.178.21:/home/pi/openhab-backup.zip
if errorlevel 1 echo SCP ist fehlgeschlagen. && goto :fehler
ssh -i .ssh\id_rsa pi@192.168.178.21 -t "restore"
if errorlevel 1 echo Restore ist fehlgeschlagen. && goto :fehler
echo Fertig.
endlocal
goto :EOF

:fehler
endlocal
exit /b 1

Linux-Seite

Datei /home/pi/.ssh/authorized_keys

command="/home/pi/openhab-backup",no-agent-forwarding,no-port-forwarding ssh-rsa AAA...

Datei /home/pi/openhab-backup

#!/bin/bash
. /etc/profile.d/openhab2.sh
#echo "Befehl: $SSH_ORIGINAL_COMMAND"
case "$SSH_ORIGINAL_COMMAND" in
    "backup")
        sudo $OPENHAB_RUNTIME/bin/backup openhab-backup.zip
        sudo chown pi openhab-backup.zip
        ;;
    "scp -f /home/pi/openhab-backup.zip")
        $SSH_ORIGINAL_COMMAND
        ;;
    "scp -t /home/pi/openhab-backup.zip")
        $SSH_ORIGINAL_COMMAND
        ;;
    "restore")
        echo  Das wird mehrere Minuten dauern...
        sudo systemctl stop openhab2
        sudo $OPENHAB_RUNTIME/bin/restore openhab-backup.zip
        sudo systemctl start openhab2
        echo Fertig.
        ;;
    *)
        echo "Access denied: $SSH_ORIGINAL_COMMAND"
        exit 1
        ;;
esac

Sinn und Zweck der Dateien

Die Datei .ssh/authorized_keys (in der ab "ssh-rsa" alles aus der id_rsa.pub von der Windows-Seite kopiert ist) sorgt dafür, dass mit diesem Schlüsselpaar nur genau das Skript openhab-backup ohne Kennworteingabe ausgeführt werden kann. Innerhalb von openhab-backup wird mit $SSH_ORIGINAL_COMMAND auf das vom Windows-Client übergebene Kommando zugegriffen. Nur ganz bestimmte Kommandos sind erlaubt. Die OpenHAB-Umgebungsvariablen werden zu Beginn aus der Datei /etc/profile.d/openhab2.sh ausgelesen, insb. damit $OPENHAB_RUNTIME definiert ist. Die scp -f und scp -t Befehle sind für Backup (Übertragung from server to client) und Restore der Datei (Übertragung from client to server) - so arbeitet scp intern.
Das Windows-Programm backup.cmd erlaubt ein Backup der OpenHAB-Konfiguration. Per ssh wird zunächst auf dem Linux-Rechner ein Backup angelegt und dieses dann per scp zum Windows-Rechner übertragen und dort im Verzeichnis backups abgelegt, wobei ein Zeitstempel im Dateinamen ergänzt wird.
Das Windows-Programm restore.cmd arbeitet sozusagen genau umgekehrt. Dabei muss aber der Dateiname der Backup-ZIP-Datei explizit angegeben werden.

Labels: , , , ,


 

Echt hart - OpenHAB, Tuya-Convert, Raspi und WLAN-Steckdosen Luminea NX-4491-675

Mühsame Erfahrungen bei der Einrichtung von WLAN-Steckdosen und OpenHAB

Um was geht's?

Erste Erfahrungen mit Heimautomation - und das möglichst ohne Cloud.

Hardware:

Vorhanden waren bereits:

Für die Installation und Konfiguration wird außerdem benötigt:
  • LAN (WLAN sowieso)
  • Handelsübliches Notebook/PC mit Windows
  • Smartphone (in meinem Fall ein Huawei P10 lite mit Android)
Neu gekauft:
  • Ein 3er-Set WLAN-Steckdosen von Pearl (ca. 35€ inkl. Versand).
    Bei Pearl werden diese als WLAN-Steckdose SF-450.avs verkauft.


    Dahinter steckt ein Luminea NX-4491-675. Das sind die einfacheren (und kompakteren) Modelle ohne Strommesser. Inzwischen bietet Pearl unter derselben Bezeichnung anders aussehende Steckdosen an - ich nehme aber an, dass sie im Prinzip genauso funktionieren.
Zwischenstand - nach 2 Tagen ziemlichen Mühen:
  • Die Steckdosen sind mit Tuya-Convert geflasht (also von der China-Cloud befreit)
  • und in OpenHAB mehr oder weniger eingerichtet
  • Ein Backup- und Restore-Skript für OpenHAB
  • Steckdosen können per Handy - nur von zu Hause aus - geschaltet werden.

Warum war das so mühsam?

Im Prinzip sollte es relativ einfach gehen:
  • Steckdosen flashen mit Tuya-Convert (OTA : over the air - ohne das Gehäuse zu öffnen). Dazu wird ein Linux-System mit WLAN benötigt.
  • Steckdosen konfigurieren
  • OpenHAB einrichten
  • MQTT bei OpenHAB einrichten (MQTT ist das Protokoll, mit dem u.a. diese Steckdosen gesteuert werden)
  • Steckdosen bei OpenHAB konfigurieren
Das hört sich doch gar nicht so schwer an.
In der Praxis sind vor allem folgende Probleme aufgetreten:
  • Zum Flashen habe ich versucht, den Raspi mit WLAN-Stick zu verwenden. Das hat beim ersten Mal auch relativ problemlos funktioniert, danach aber nie wieder! Es traten dann wahlweise folgende Fehler auf (die zu absolut verwirrenden Fehlermeldungen führten):
    • Der WLAN-Stick wurde nicht erkannt
    • Das WLAN vtrust-wlan war gar nicht sichtbar auf dem Smartphone
    • Das WLAN vtrust-wlan war sichtbar, man konnte sich aber mit dem Smartphone nicht damit verbinden
    • Das Skript sagte immer wieder "Retrying"
    • Da der Start von OpenHAB den Raspi für gut 10 Minuten extrem belastet, ist ein Reboot (um etwas anderes auszuprobieren) eine zeitraubende Angelegenheit.
  • OpenHAB
    • OpenHAB ist für Einsteiger alles andere als einfach.
    • Die Konfiguration ist m.E. völlig wirr - Textdateien und/oder per GUI (wobei diese Einstellungen dann nicht in den Textdateien landen)
    • Außerdem ist die Dokumentation, die man im Internet so findet, häufig veraltet und funktioniert so nicht mehr.
  • Doofe Kleinigkeit: Ich hatte ein Leerzeichen in der SSID! Damit kommt die Tasmota-Software einfach nicht klar! Es gibt auch keinen Workaround. Daher habe ich die SSID in der Fritzbox umbenannt und einen kurzen Namen ohne Leerzeichen oder Sonderzeichen gewählt. Das zog natürlich diverse Nacharbeiten bei den dutzenden vorhandenen Geräten (Smartphones, Tablets, ...) nach sich.
Meine Empfehlungen daher: 
  • Den Raspi nur für OpenHAB verwenden, nicht für das Flashen.
  • Zum Flashen besser ein Linux im VMWare Player verwenden.

So, jetzt noch einmal alles hintereinander in Kurzform

Das ist als Kochrezept gedacht, man muss schon wissen was man da tut...

Vorüberlegungen

Schon vorher sollte klar sein, welchen Rechner man hinterher nutzen möchte, um die Heimautomation laufen zu lassen - das kann der Raspi sein, wobei ein Raspi 2 zumindest beim Start schon ordentlich rödelt, aber wenn er einmal läuft, ist das sicher eine gute Variante, da klein und sparsam.
Die sichtbare Oberfläche des Ganzen läuft sowieso im Browser auf dem Windows-PC.
Ich habe mich (vorerst) entschieden, das Ganze auf dem Rapi laufen zu lassen.
Benötigt werden zwei Dinge: Ein MQTT-Broker und die eigentliche SmartHome-Steuerungssoftware (in meinem Fall OpenHAB2).

Raspi installieren

Man kann es sich evtl. einfach machen mit OpenHABian, ich habe aber das normale Raspberry Pi OS "Buster" verwendet. Um das OS auf den RASPI zu bringen, braucht man eine SD-Karte. Wenn der Raspi am LAN (nicht WLAN) hängt, ist die Installation ziemlich simpel und es gibt genug Tutorials dazu.
Zu bedenken ist: Man sollte schon auf dem Windows PC eine leere Datei namens ssh auf der SD-Karte anlegen, damit später der Shell-Zugang per SSH vom Windows-PC aus möglich ist. Das ist die einfachste Möglichkeit, da dann zu keinem Zeitpunkt Monitor und Tastatur an den Raspi angeschlossen werden müssen.

Den Raspi konfigurieren

Das kann nun schon vom Windows-Rechner aus erfolgen (per ssh pi@raspberrypi o.ä., siehe https://braspi.de/blogs/braspi-blog/raspberry-pi-ersteinrichtung-ohne-monitor-und-tastatur
Mit raspi-config Details einstellen usw., siehe Anleitung auf der o.g. Web-Site.
Bei mir heißt der Raspi-Rechner übrigens oha. Nach der Konfiguration und Reboot des Raspi ist also die Shell über ssh pi@oha zu erreichen.

Dem Raspi eine feste IP zuordnen

Ganz wichtig: Über die FritzBox dem Raspi eine feste IP-Adresse zuordnen (in meinem Fall 192.168.178.21). Wir wollen später hier einen MQTT Broker installieren und auch die SmartHome-Software. Für den MQTT Broker ist die fixe IP-Adresse zwingend, für die SmartHome-Software auch vorteilhaft.

MQTT Broker installieren

Auf dem Raspi dem MQTT-Broker "mosquitto" installieren und dafür sorgen, dass er beim Systemstart auch als Dienst automatisch gestartet wird.
sudo apt-get install mosquitto mosquitto-clients
sudo systemctl enable mosquitto.service

OpenHAB2 installieren

TODO genauere Erklärung.
Die Installation war definitiv etwas fummelig. 
Das erste Problem war, dass java nicht funktionierte. Hier musste auf ein anderes Java gewechselt werden. Außerdem ist es auch danach nicht trivial.
Ich habe es so ähnlich gemacht wie unter https://openhabdoc.readthedocs.io/de/latest/Raspberry/ beschrieben.
Ein einfaches apt-get install wäre zu schön gewesen...

OpenHAB2 als Dienst einrichten. Ob das wirklich manuell nötig ist, weiß ich nicht. Ich habe jedenfalls ein sudo systemctl enable openhab2.service gemacht.

Steckdosen flashen

Die Steckdose noch nicht einstecken!
Auf dem Notebook/Windows-PC
  1. Ein aktuelles Ubuntu Linux ISO-Image herunterladen
  2. Den VMWare Player herunterladen und installieren
  3. Mit dem VMWare Player das Linux  als virtuelle Maschine installieren
  4. Das Ubuntu halbwegs brauchbar einrichten (z.B. deutsche Tastatur) und auf den aktuellen Stand bringen
  5. Im Ubuntu der Anleitung von https://github.com/ct-Open-Source/tuya-convert folgen - aber nur die Installation, nicht Flashen! Beim install-prereq.sh stattdessen mit sudo ./install-prereq.sh arbeiten
  6. Den WLAN USB-Stick einstöpseln und -nur- mit der Ubuntu VM verbinden (nicht mit dem Host)
  7. Im Ubuntu mit iw dev herausfinden, wie der WLAN-Stick im Linux heißt, und das in der Datei config.txt entsprechend anpassen (wlan0 durch den passenden Namen ersetzen).
  8. Jetzt erst sudo ./start_flash.sh ausführen. Am Anfang einmal yes [Enter], die Fragen ob dies und das gestoppt werden soll, mit y beantworten.
  9. Nun auf dem Smartphone mit dem WLAN vtrust-wlan verbinden. Falls das nicht funktioniert, abbrechen. Am besten den WLAN-Stick wieder herausziehen, die Ubuntu-VM neu booten und ab Schritt 6 nochmal von vorne versuchen.
  10. Jetzt die Steckose einstecken und durch langes Drücken der Taste in den Pairing-Modus versetzen (die LED blinkt dann)
  11. Dann erst Enter drücken.
  12. Bei der Frage welches ROM installiert werden soll, "tasmota.bin" auswählen und das Ganze nochmal bestätigen.
Die Steckdose ist jetzt geflasht, aber sie wird nicht funktionieren - keine Panik.

Steckdose konfigurieren

Die Steckdose macht jetzt ein eigenes WLAN auf. Das heißt tasmota-irgendwas. Achtung: Anscheinend gibt es eine dabei Zeitbegrenzung (siehe https://tasmota.github.io/docs/Getting-Started/ unter "Initial Configuration")
Auf dem Windows-PC mit diesem WLAN verbinden.
  1. Es erscheint eine Konfigurationsseite im Browser. Falls das nicht funktioniert, manuell zu http://192.168.4.1 wechseln
  2. Es erscheint die Startseite der Steckdose:
  3. Hier nun als allererstes die WLAN-Zugangsdaten bei "AP1 SSID" und "AP1 Password" eingeben und vor dem Drücken auf Save sehr sorgfältig kontrollieren. Nicht den Haken bei der Checkbox "AP1 Password" vergessen.
  4. Beim PC wieder mit dem normalen WLAN verbinden.
  5. Die Steckdose startet neu und ist nun im WLAN irgendwo sichtbar als "tasmota-irgendwas" oder aber über den im Konfigurationsdialog festgelegten Hostnamen. In meinem Fall kann man das über die FritzBox leicht herausfinden (es gibt sicher auch andere Möglichkeiten). Die verwendete IP-Nummer merken (z.B. 192.168.178.44).
  6. Diese IP im Browser aufrufen: http://192.168.178.44. Herzlichen Glückwunsch, die Steckdose ist im WLAN!
  7. Es erscheint wieder eine Konfigurationsseite.
    Diese sieht "oben rum" zunächst noch nicht ganz so aus wie in diesem Screenshot:
  8. Das Menü "Configuration" aufrufen:



  9. "Configure WiFi" aufrufen und den Hostnamen ergänzen.

     Der Hostname muss natürlich im lokalen Netzwerk eindeutig sein und (wenn ich die Überschrift bei dem Eingabefeld richtig deute)  sollte er aus einem Text, Bindestricht und dann vier Ziffern bestehen. Keine Ahnung, ob das wichtig ist. Natürlich sollte der Hostname keine Sonderzeichen (außer Minus und Unterstrich) oder Leerzeichen enthalten. Auf "Save" drücken.
  10. Die Steckdose startet neu und ist nun im WLAN unter dem gewählten Hostnamen sichtbar. Im Browser also http://hostname (z.B. http://steckdose-0001) eingeben und es erscheint wieder die Startseite:

  11. Wieder auf "Configuration" gehen und dort auf "Configure Template". Als Name für das Template etwas passendes auswählen. Ich habe "Luminea NX-4491-675" eingegeben, das wurde aber beim Speichern offensichtlich gekürzt. Als Basis für das Template Generic (18) auswählen. Falls man großes Glück hat, dann gibt es vielleicht schon ein passendes Template in der Werteliste (je nach Hardware). In dem Fall kann natürlich zunächst dieses Template ausprobieren (und den weiteren Verlauf abkürzen).
    Was wir jetzt zu tun haben, ist, die logischen Ein-Ausgabe-Pins des Chips mit den richtigen Stellen zu verbinden - ohne diese Konfiguration bliebe die Steckdose "tot".
  12. Dank eines YouTube-Videos ist es gar nicht so schwierig, das herauszufinden. In meinem Fall ist es eine einfache Steckdose, sie hat 1 LED und einen physikalischen Button. Richtigerweise brauchen wir also Zuordnungen für Button1, Led1 und Relay1 (das Relay ist der eigentliche Schalter). Zuerst setzen wir bei allen GPIOs die Zuordnung auf None (0). Dann schließen wir eine einfache Lampe an die Steckdose an (und stellen vorher an einer normalen Steckdose sicher, dass die Lampe leuchten sollte!). Nun setzen wir bei 4 GPIOs (z.B. 12, 13, 14, 15) die Zuordnung auf Relay1 .. Relay4 und drücken "Save".
    Die Steckdose startet neu und zeigt wieder die Startseite an. Auf der Startseite gibt es jetzt 4x nebeneinander den Text "OFF" und darunter eine Schaltfläche "Toggle" (so ähnlich wie im Screenshot oben, aber eben 4x nebeneinander). Nun testet man, was mit der Steckdose und der Lampe passiert, wenn man auf die "Toggle" Schaltflächen klickt. Wenn bei einer der Schaltflächen (z.B. in meinem Fall bei der vierten) die Lampe an- bzw. ausgeht, dann wissen wir, dass die GPIO-Nummer eigentlich Relay 1 ist und nicht Relay 4. Mit etwas Glück (so wie in meinem Beispiel) stellen wir außerdem fest, dass - nur wenn die Lampe nicht leuchtet - eine der Schaltflächen die Kontroll-LED umschaltet.
    Wir gehen wieder zum "Configure Template" Bildschirm und legen für die 4 getesteten GPIOs nun die richtige Zuordnung fest -wobei man statt Led1 auch LEd1i ("i" wie invertiert) verwenden kann, je nachdem wie man es lieber hat). Die übrigen GPIOs setzt man wieder auf None:

    Danach klickt man auf "Save" und die Steckdose startet neu. Im Hauptmenü ist jetzt nur noch ein ON/OFF-Status und eine "Toggle"-Schaltfläche zu sehen. Diese kann man nun testen - sie sollte die Lampe ein- und ausschalten.
    Auf ähnliche Weise (Button1 .. Button4 testweise unbenutzten GPIOs zuordnen) findet man heraus, welche GPIO-Nummer tatsächlich zu Button1 gehört. Dazu wechselt man nach dem Konfigurieren und Neustart zur "Console" und beobachtet, welche Nachricht (außer den Power-Nachrichten) ankommt, wenn man auf den Button klickt (wenn ich mich recht erinnere, im Youtube-Video ist das jedenfalls gut erklärt).
  13. Es kann sein, dass man nach dem ersten Erstellen des Templates (Schritt 12) bei "Configure Module" zunächst einmal das neu angelegte Template (in meinem Fall "Luminea NX-449") auswählen muss.
  14. Unter "Configure Other" wählt man nun einen passenden Namen aus und achtet darauf, dass "MQTT Enable" angehakt ist:


  15. Es kann sein, das man nach dem ersten Erstellen des Templates (Schritt 12) bei "Configure Module" zunächst einmal das neu angelegte Template (in meinem Fall "Luminea NX-449") auswählen muss.
  16. Unter "Configure MQTT" wählt man jetzt (das ist wichtig, damit die Steckdose nicht nur über dieses Web-UI, sondern auch über MQTT angesprochen werden kann) die IP-Nummer des Raspi bzw. genauer gesagt die IP-Nummer des Rechners auf dem der MQTT-Broker läuft. Außerdem habe ich die Einträge Topic und Client beide auf einen einfachen eindeutigen Namen "steckdose1" geändert:

LED-Statusanzeige anpassen

In der "Console" habe ich über den Befehl ledstate 7 den LED-Status so konfiguriert, dass die LED sich nun so verhält:
  • Die LED leuchtet, wenn der Strom aus ist (sozusagen als Mini-Nachtlicht) und die LED ist aus, wenn der Strom an ist.
  • Die LED blinkt, wenn MQTT-Nachrichten empfangen werden.
Andere sinnvolle LED-States sind z.B. 0  (LED immer aus). Das Verhalten hängt sicher auch davon ab, ob man "Led1" oder "Led1i" konfiguriert hat.

MQTT testen

Mit einem Tool wie dem Google-Browser-Addon MQTTLens kann man nun beobachten, dass die Steckdose alle 5 Minuten ihren Status meldet. Dazu zuerst die Connection zum Raspi hinzufügen und dann das Topic "#" subscriben.
Außerdem kann man mit "Publish" und dem Topic steckdose1/power und der Message on oder off die Steckdose ein- und ausschalten. Die richtigen Topics lassen sich herausfinden über die Menüs "Information" und "Console" der Steckdosen-Seite.
Mir ist heute aufgefallen, das MQTTLens immer wieder die Verbindung zum Broker verliert und gleich wieder neu aufbaut!?
Ich ignoriere das vorerst mal. Scheint daran zu liegen, dass ich den Rechnernamen "oha" anstelle der IP-Nummer 192.168.178.21 verwendet hatte.

Meilenstein erreicht: MQTT funktioniert

Bis hierhin war es relativ unabhängig von der Heimautomations-Software OpenHAB 2. Der Rest der Konfiguration folgt in einem weiteren Artikel.

Labels: , ,


12. Februar 2011

 

Rusty's blog: Not doing much

Trying to cross-compile Python for the Wii in an attempt to let my pygame-based simple game run on the Wii...
(somebody has done that before, search for Alien Breed Wii on the web).
On ubuntu, when I run cross_compile_python.sh, somere during the build the script throws a lot of error messages and then hangs.

./configure.lineno: line 429: expr: command not found (many times), followed by ./configure.lineno: fork: Cannot allocate memory

Someone else had a similar problem in 2009, but didn't publish a solution: See
Rusty's blog: Not doing much from Sunday, 31 May 2009.

9. Dezember 2007

 

Panda3D - Spiegelungen

Habe gestern und heute versucht, in das Spielchen Spiegel einzubauen.
Schließlich sind die Gegner derzeit Pinguine, und da wäre eine spiegelglatte Eis-Oberfläche doch irgendwie passender als der Rasen.

Aber so ganz hab ich es noch nicht hingekriegt.

Es gibt 2 Probleme (das 2. ist sicherlich einfach zu lösen)

1. Problem:

Die Spiegelung funktioniert zwar im Prinzip, ist aber verzerrt und entspricht nicht den Erwartungen.
Die beigefügten Screenshots verdeutlichen das Problem. Die Spielfigur ist immer im Zentrum und wird optisch korrekt gespiegelt. Aber alles, was außerhalb des Zentrums ist, wird nicht richtig gespiegelt - man achte auf die Pinguine.







Außerdem ist die Pixelauflösung der Spiegelung zu gering (wenn ich es mit 1024x1024 statt 512x512 versuche, stürzt Panda3D ab - vermutlich unterstützt die Laptop-Grafikkarte das nicht. Was gibt es hier für Alternativen?

2. Problem:

Der gespiegelte Himmel ist zu dunkel, obwohl er weiß ist. Das liegt aber sicher daran, dass das AmbientLight zu dunkel ist und das DirectionalLight eben von oben nach unten scheint. Ich müsste also vermutlich eine Lichtquelle einbauen, die sich nur auf den Himmel auswirkt.

Der nächste Schritt wäre dann vielleicht noch eine Textur für den Himmel.

22. Oktober 2007

 

PDF 2-Up Script mit pyPdf

Mit dem folgenden Skript (sicherlich noch ausbaufähig!) kann man eine PDF-Datei ins 2-Up Format umwandeln (d.h. 2 Seiten auf eine):

#!/bin/env/python
# -*- coding: utf-8 -*-

# Basiert auf der pyPdf-Library von Mathieu Fenniak
# Siehe http://pybrary.net/pyPdf
#
# (C) Henning von Bargen 2007
#
import os
import sys
import unittest
from cStringIO import StringIO

from pyPdf import PdfFileWriter, PdfFileReader

from reportlab.lib.units import cm, mm, inch
from reportlab.lib import pagesizes
from reportlab.pdfgen import canvas

from pyPdf.pdf import *
from pyPdf.generic import *


PAGESIZE = pagesizes.landscape(pagesizes.A4)

def framePage(canvas, title):
canvas.saveState()
canvas.setFont('Times-BoldItalic',12)
canvas.drawCentredString(PAGESIZE[0] * 1/2, 200*mm, title)
canvas.setFont('Times-Roman',10)
canvas.drawCentredString(PAGESIZE[0] * 1/4, 10*mm,
'Page %d' % (canvas.getPageNumber() * 2 - 1))
canvas.drawCentredString(PAGESIZE[0] * 3/4, 10*mm,
'Page %d' % (canvas.getPageNumber() * 2))
canvas.restoreState()
#canvas.doForm("frame") # Hier wäre Platz für ein Logo

def makeEmptyPagesPdf(nPages):
buffer = StringIO()
c = canvas.Canvas(None)
c.setPageSize(PAGESIZE)
#c.setPageCompression(0)
#c.setPageCallBack(pageCallBack)
#framePageForm(c) # define the frame form
c.showOutline()
for page in range(nPages):
print page
framePage(c, "This is a title")
c.showPage()
buffer.write(c.getpdfdata())
buffer.seek(0)
return buffer


def _pushTransformPopGS(contents, offset, scale, pdf):
# adds a graphics state "push" and "pop" to the beginning and end
# of a content stream. This isolates it from changes such as
# transformation matricies.
print "offset:",offset
print "scale:",scale
stream = ContentStream(contents, pdf)
stream.operations.insert(0, [[], "q"])
arr = [scale[0],0,0,scale[1],offset[0],offset[1]]
arr = [FloatObject(str(x)) for x in arr]
stream.operations.insert(1, [arr, "cm"])
stream.operations.append([[], "Q"])
return stream

def mergePage(page1, page2, offset, scale):

newResources = DictionaryObject()
rename = {}
originalResources = page1["/Resources"].getObject()
page2Resources = page2["/Resources"].getObject()

for res in "/ExtGState", "/Font", "/XObject", "/ColorSpace", "/Pattern", "/Shading":
new, newrename = PageObject._mergeResources(originalResources, page2Resources, res)
if new:
newResources[NameObject(res)] = new
rename.update(newrename)

# Combine /ProcSet sets.
newResources[NameObject("/ProcSet")] = ArrayObject(
ImmutableSet(originalResources.get("/ProcSet", ArrayObject()).getObject()).union(
ImmutableSet(page2Resources.get("/ProcSet", ArrayObject()).getObject())
)
)

newContentArray = ArrayObject()

originalContent = page1["/Contents"].getObject()
newContentArray.append(PageObject._pushPopGS(originalContent, page1.pdf))

page2Content = page2['/Contents'].getObject()
page2Content = PageObject._contentStreamRename(page2Content, rename, page1.pdf)
page2Content = _pushTransformPopGS(page2Content, offset, scale, page1.pdf)
newContentArray.append(page2Content)

page1[NameObject('/Contents')] = ContentStream(newContentArray, page1.pdf)
page1[NameObject('/Resources')] = newResources

def write2Up(infname, outfname):

output = PdfFileWriter()
input = PdfFileReader(open(infname, "rb"))
nPages = input.getNumPages()
print "nPages=",nPages
nNupPages = int((nPages+1)/2.0)
print "nNupPages=",nNupPages
emptyPages = PdfFileReader(makeEmptyPagesPdf(nNupPages))

page = None
nupindx = 0
for indx in range(nPages):
print "processing page", indx
if indx % 2 == 0:
if page is not None:
output.addPage(page)
print "nupindx=",nupindx
page = emptyPages.getPage(nupindx)
nupindx += 1
mergePage(page, input.getPage(indx),
(PAGESIZE[0]/2 * (indx%2), 0), # offset x,y
(0.7, 0.7) # scaling x,y
)
if page is not None:
output.addPage(page)
outputStream = open(outfname, "wb")
output.write(outputStream)


if __name__ == "__main__":
infname = sys.argv[1]
if len(sys.argv) > 2:
outfname = sys.argv[2]
else:
outfname = os.path.splitext(infname)[0] + "-new.pdf"
write2Up(infname, outfname)

10. September 2007

 

Einen habich noch: Python 2-Zeiler

Musste kürzlich einen Haufen TrueType-Zeichensätze per Fernwartung installieren.
Es gab aber keine Möglichkeit, Dateien zu kopieren.
Nur die Zwischenablage ging, und das auch nur für Texte.
Aber Python ist dein Freund:
  1. Dateien zusammenzippen zu datei.zip,
  2. in Python: import uu; uu.encode(open("datei.zip","rb"), open("datei.txt","wt"))
  3. Die Datei datei.txt dann per Editor und Zwischenablage blockweise (ging nur bis 64K) auf den Zielrechner kopieren.
  4. Auf dem Zielrechner in Python: import uu; uu.decode(open("datei.txt","rt"), open("datei.zip","wb"))
  5. Und dann datei.zip wieder entpacken.

Das Zippen habe ich mit WinZip gemacht, aber auch das wäre leicht mit Python gegangen. Ich musste mir nur leider in weniger als 10 Minuten eine Lösung einfallen lassen, und da geht Geschwindigkeit eben vor Eleganz.

Da hat Python mir den Tag gerettet...


 

Dies und Das

Blogs sind eigentlich langweilig. WikiWikis sind doch irgendwie netter.
Mein Favorit derzeit ist MoinMoin (http://www.moinmoin.wikiwiweb.de/).

Erstens, weil's in Pure Python programmiert ist.
Zweitens, weil es leicht erweiterbar ist.
Drittens schon wegen des freundlichen Namens, bei dem ich gleich an Urlaub denken muss...
Und viertens, es kommt ohne Datenbank aus.

Zur Musik: Hab gerade im Radio "I've been Everywhere" von Jackie Leven gehört (vom Album "Oh What A Blow That Phantom Dealt Me!" - das scheint auch so eine Art Tribut an Johnny Cash zu sein. Klasse!

Rund um Python:

Thread-Vererbung will ich in Python 2.6 einbauen, wenn ich denn mal Zeit dafür habe...

Panda3D: (http://www.panda3d.org/) hat ne neue Version rausgebracht und mein PC hat jetzt ne Karte mit Bump Mapping. Mein 3D-Pacman-Klon mit dem guten deutschen Arbeitstitel "Arbeitstitel-Mann" ist gut spielbar, aber auch da fehlt mir die Zeit. Zuletzt habe ich es immerhin geschafft, eine Highscore-Liste einzubauen. Außerdem "Einbahnstraßen-Extras". Nächste Schritte sind Bump-Mapping, noch mehr Extras und dann natürlich intelligentere Gegner. Ein echter Netzwerkmodus wär auch schön, steht aber auf der Liste weit hinten (zu zweit an einem Rechner macht es doch am meisten Spaß). Werde es wohl mal veröffentlichen, wenn ich dazu komme - dazu fehlt vor allem noch eine Installationsanleitung.

Die Silbentrennung (http://deco-cow.sourceforge.net/) könnte auch mal wieder Pflege vertragen, wenn mal wieder Zeit ist:
* sauberere Vererbung von RL-Standardklassen
* Integration des anscheinend ziemlich guten hunspell (http://hunspell.sourceforge.net/).

14. Juni 2007

 

Früher...

Hab heute im Radio "Shine On" von einer Band mit einem unaussprechlichen Namen gehört, und das hat mich doch stark an früher erinnert.
Google ist dein Freund - tatsächlich, das Lied war im Original von "House of Love", einer Band, die damals vor langer Zeit (ca. 1988) als eine von den Manchester-Bands bezeichnet wurde.
Ach war das schön damals... Ich hab sie noch im Dortmunder Live Station gesehen - ein Konzert in fast familiärer Athmosphäre war das.
Und YouTube ist auch dein Freund - dort findet man tatsächlich ein Video von House of Love - Shine On. Sehr schön auch "Man To Child".

Und in der Bücherei hab ich nochmal zwei alte Romane von der Scheibenwelt ausgegraben, die aus heutiger Sicht noch irgendwie unfertig wirken:

Aber ich konnte mich halt an die Bücher nur noch sehr schwach erinnern...


 

Programmieren mit Panda

So, heute habe ich das Spiel soweit hingekriegt, dass man mehrere Level nacheinander spielen kann und dabei die PUnkte und Anzahl Leben erhalten bleiben (sollte eigentlich selbstverständlich sein, ich weiss).
Im Prinzip war es ein Refactoring.
Zunächst wollte im zweiten Level die Steuerung nicht funktionieren, und es waren irgendwelche Phantom-Objekte vorhanden.
Beides lag daran, dass ich die Objekte im Baum "render" und die Tasks im globalen TaskManager "taskMgr" beim Ende des ersten Levels bzw. beim Start des nächsten Levels nicht richtig gelöscht bzw. neu angelegt hatte.
Die einfachen Befehl
render.ls()
print taskMgr
haben aber sehr dabei geholfen, dass rauszufinden - obwohl mir immer noch nicht klar ist, warum Tasks, die ich soeben entfernt habe, von print taskMgr immer noch aufgelistet werden.

Weil Python so viel Spass macht und ich das händische Kopieren leid war, habe ich mir auch noch gleich ein kleines Backupskript backup.py angelegt:
#!/bin/env python
# -*- coding: iso-8859-1 -*-
import os

import os.path
import glob
import time
import shutil
dateStr = time.strftime("%Y%m%d-%H%M%S")
thisDir = "."

backupDir = os.path.join (thisDir, "backup", dateStr)
try: os.mkdir(backupDir)

dateien = os.listdir(thisDir)
numFiles = 0
for datei in dateien:
fullname = os.path.join (thisDir, datei)
if os.path.isfile(fullname):
base, ext = os.path.splitext(datei)
if ext not in [".log", ".py", ".obj"]:
print "Datei gesichert:", datei
shutil.copy2 (os.path.join (thisDir, datei), backupDir)
numFiles += 1
else:
print "Unterverzeichnis ignoriert:", datei
_ = raw_input("Backup erledigt (%d Dateien gesichert)" % numFiles)
except:
import traceback
traceback.print_exc()
_ = raw_input ("Backup konnte nicht komplett durchgeführt werden!")

12. Juni 2007

 

Spieleprogrammierung mit Panda3D

Ich habe mich jetzt einige Tage (wo es so schön geregnet hat) mit Panda3D beschäftigt, und ich muss sagen ich bin davon ganz angetan - nicht unbedingt begeistert, aber es ist schon toll.

Als erste Übung will ich jetzt mal eine Art 3D-PacMan bauen, um mal in das Ganze so reinzukommen.
Aller Anfang ist schwer, aber jetzt habe ich den Punkt erreicht, wo ein Programm grundsätzlich funktioniert und es scheinbar immer einfacher wird, neue Features einzubauen.
Immerhin findet mein Sohn (7) das Spiel schon so schön, dass er ständig quengelt weil er spielen möchte.

Am meisten Zeit ist bisher für die Collision Detection draufgegangen, weil die zwar grundsätzlich funktioniert, dann aber scheinbar etwas falsche Werte zurückliefert, so dass z.B. der Avatar leicht in die Wände hinein rennt und dann zu weit zurückprallt und sich die Gegner ständig in den Ecken "verheddert" haben.
Für die Gegner habe ich es inzwischen durch gute alte handgestrickte einfachste Geometrie-Routinen in den Griff gekriegt, und beim Avatar stört es nicht ganz so sehr.

Über den weiteren Fortschritt werde ich hier gelegentlich berichten.

 

Multi-Threading ist böse - Multi-Threading is evil

Nach inzwischen einigen Monaten Erfahrung kann ich den vielen klugen Leuten nur beipflichten, die der Meinung sind, dass man sich mit Multi-Thrading-Programmen nur Ärger einhandelt - egal in welcher Programmiersprache sie geschrieben sind.

Bei Python gibt es da zwei spezielle Probleme:

Neben diesen Python-spezifischen Problemen gab es am Anfang die üblichen Schwierigkeiten mit

Viel gemeiner und bei weitem schwieriger zu entdecken waren aber die Probleme, die durch Race Conditions auftreten können, wenn ein Thread gerade eine Datei schreibt und ein anderer Thread in dieser Zeit einen Unterprozess startet, z.B. mit dem subprocess-Modul. Der Unterprozess erbt dann das Datei-Handles (unter Windows), woran natürlich niemand denkt. Dadurch ist die Datei für Windows quasi so lange zum Schreiben geöffnet, wie der Unterprozess läuft - z.B. kann der Prozess, der die Datei eigentlich geschrieben hat, die Datei anschließend nicht mit os.rename umbenennen. (Ja ja, ans zwischenzeitliche close habe ich gedacht).

Die Ursache ist die, dass alle Handles an Unterprozesse vererbt werden, wenn es nicht für das Handle explizit ausgeschaltet wird. Jedes Handle mit dem normalen open(...) wird also vererbt.

Als Workaround muss dann anstelle von outf = open (fname, "w") der Aufruf outf = os.fdopen (os.open (fname, os.O_WRITE + os.O_NOINHERIT + ....), "w") verwendet werden.


12. September 2006

 

Die Gelehrten der Scheibenwelt

Das Buch hab ich mir neulich geholt, als ich auf Geschäftreise war und nix zu lesen hatte.
Ehrlich gesagt war es das erste Buch von Terry Pratchett, das mir nicht gefallen hat. Dieser Mischmasch aus Erklärung unserer Rundwelt und dann wieder 3 Seiten Roman, was die Zauberer der Unsichtbaren Universität so treiben - das ist nichts Halbes und nichts Ganzes.
Der populärwissenschaftliche Teil ("Lügen für Kinder") an und für sich ist ja noch ganz nett gemacht, aber die Romankapitel sind einfach nur langweilig - das gab es bei Terry Pratchett eigentlich noch nie.

Und außerdem störe ich mich gelegentlich an der deutschen Übersetzung, aber das ist sicherlich Geschmackssache. Ich finde eben "L-Space" einfach netter als "B-Raum".

 

Tagebuch führen ist gar nicht so einfach

Ich verstehe nicht, warum ich die eigenen Beiträge beim Suchen nicht finde.
Wenn ich sie selbst schon nicht finde, wie soll dann jemand anderes die finden?
Jetzt habe ich mal Titel hinzugefügt. Ob's was hilft? Ich glaube eher nicht.

 
Mann, jetzt habe ich schon ein ganzes Jahr nix mehr reingeschrieben.
Weder Zeit noch Lust gehabt. Das Leben rauscht nur so vorbei.
Eigentlich wollte ich diesen Sommer einen Artikel über Python und Oracle für http://otn.oracle.com schreiben, aber auch dafür hat die Zeit nicht gereicht.
Stattdessen nehme ich nochmal meine Silbentrennung (http://deco-cow.sourceforge.net) in Angriff.
Reportlab hat ja jetzt alles auf Unicode umgestellt, arbeitet aber anscheinend intern mit utf8. Das mag die Silbentrennung gar nicht. Im Moment funktioniert daher fast gar nichts.
Mal überlegen, ob ich die Silbentrennung intern auch komplett auf Unicode umstelle und vielleicht nebenbei auch die ganze Regelverarbeitung ändere, oder ob ich umgekehrt lieber alles was an die Silbentrennung geht, nach iso-8859-1 codierere und auf dem Rückweg wieder nach utf8.
Alles nicht so einfach.

1. September 2005

 

Python und Windows Dienste (Services)

Gleich mal das erste zum Thema Python und Windows Dienste
(Python and Windows Services)
Unsinnigerweise sendet Windows an manche Programme ("Konsolenprogramme") ein Signal (CTRL_LOGOFF_EVENT), wenn sie als Dienst gestartet sind und sich ein Benutzer abmeldet. Welcher Benutzer sich abmeldet, erfährt das Programm aber nicht.
Wer also vorhat, Windows-Dienste mit Python zu entwickeln, aufgepasst!
(Dasselbe gilt übrigens auch für JAVA)
Bestandteil des Tests sollte folgender Testfall sein:
  1. Dienst läuft korrekt.
  2. Benutzer meldet sich an
  3. Benutzer meldet sich wieder ab
  4. Von außen prüfen, ob der Dienst immer noch korrekt läuft.
Falls nicht, liegt wahrscheinlich das oben genannte Problem vor:
Windows sendet ein CTRL_LOGOFF_EVENT.
Python steckt gerade in einem time.sleep oder wait oder was auch immer.
Das Programm (in Multi-Threaded-Programmen nur der Hauptthread!!!) läuft dann auf eine Exception IOError: Interrupted System Call.
Alle Versuche, mit Windows-APIs wie SetConsoleCtrlHandler was zu machen, haben in meinem Fall nicht geholfen.
Tief in den Python-Bibliotheken versteckt (im Quelltext von timemodule.c) sieht man, dass die Implementierung von sleep mit MsgWaitForSingleObject arbeitet und eine Exception schmeißt, wenn dabei kein Timeout auftritt.
Besonders ärgerlich ist es, wenn dann bei Multi-Threading-Programmen der Hauptthread endet, die anderen Threads aber weiter laufen (oder kann man da generell was machen?),
und das Programm dann irgendwie in der Luft hängt.
Lösung/Solution:
Bei allen Vorkommen von time.sleep, evt.wait() etc (im Hauptthread des Programms) mit einem IOError rechnen und diesen abfangen:

try:
time.sleep(1)
except IOError:
log.warn ("Error in sleep - probably caused by CTRL_LOGOFF_EVENT")

This page is powered by Blogger. Isn't yours?