Blog: Latest Entries (15):


Was man heute unter KI versteht...

Ein sehr guter Wired-Artikel Auch 2029 wird es keine Künstliche Intelligenz geben, die diesen Namen verdient, der endlich mal klarstellt, dass das was man seit kurzen überall als KI verkauft, nichts weiteres ist als klassische Mustererkennung ist, wie man sie seit Anfang der 90er nutzt. Der klassische KI-Begriff, in dem um die Schaffung einer Intelligenz geht, hat damit garnichts zu tun.

Mich stört es wenn Firmen von KI sprechen und man dahinter nur einfache Datenanalyse mit stupiden Bedingungen und Gewichtungen erkennt. Vektoren zum Vergleichen mit bekannten Mustern sind jetzt auch nicht wirklich neu oder irgendwie intelligent.

Klickbar oder Messbar?

Am Ende eines Sprints soll etwas heraus kommen, was man dem Kunden zeigen kann bzw deliverable ist. Kunden an sich können nur sehen was klickbar ist. Dinge die nicht klickbar sind sind für die meisten Kunden einfach nicht existent, weil diese zu meist keine technischen Anforderungen stellen sondern fachliche. Man Klickt etwas an und es passiert was und das ist das was der Kunde sieht und bewertet.

Aber muss es so sein? Es gibt viele Dinge innerhalb eines Sprints, die die Entwicklung und technische Basis voran bringen ohne das ein Anwender sie bemerkt oder sehen kann. Deswegen ist es vielleicht falsch nur den Kunden die Bewertung zu überlassen. Der Kunde muss nur am Ende etwas sehen, das man messen und dann validieren kann.

Angenommen, ich möchte mehr Automatisierung in der UI haben. Formulare sollen sich automatisch erzeugen und nicht mehr per Hand zusammen gebaut werden müssen. Kann ich das in einem Sprint nach der Definition liefern? Ja! Ich baue es ein und liefere dem Kunden am Ende des Sprints einen Mess-Fall. Genau so ob die Änderung die der Kunde sieht er nach dem Spring validiert werden kann, ob der erhoffte Impact eintritt, kann meine technische Änderung genau so zur selben Zeit getestet werden. Zwar nicht durch einen Anwender sondern durch die Daten des nächsten Sprints selbst, aber das Ergebnis ist das Selbe.
Ich liefere etwas und gebe dem Kunden etwas an die Hand, um die Hypothese auf der dieses gelieferte fußt, prüfen zu können. Ich sage, ich kann mit dieser Änderung im nächsten Sprint 6 neue Formulare in der UI liefern und nicht mehr nur 2. Das ist eine gute Annahme, die sich überprüfen lässt. Ob eine andere Userstory, die vom Kunden selbst kommt, am Ende das erhoffte liefert, weis man genau so wenig, wie diese Annahme, mit der ich den Form-Generator eingebaut habe.

Nach dem folgenden Sprint, gehe ich wieder zum Kunden und zeige ihm, was wir an Formularen gebaut haben. Nun prüft er, ob meine Annahme mit 6 Formularen erfüllt wurde. Vielleicht sind es nur 5 geworden oder wir hatten sogar Zeit über, in der wir noch ein Formular mehr hätten machen können... das ist gar nicht ganz so wichtig, weil sich heraus gestellt hat, dass es eine Verbesserung gab und damit meine technische Userstory erfüllt werden konnte und nicht nochmal neu bearbeitet werden oder als Fehlschlag einzustufen ist.

Ob man also klickt oder am Ende Zahlen vorlegt, ist für den Kunden dann egal. Man hat jeweils etwas gebaut, was sich in der Zeit danach bewähren muss und nur das was dann da heraus kommt zählt und nicht was am Ende des Sprint sichtbar war. Deswegen muss ich am ende des Sprints immer nur etwas liefern, was messbar ist. Nicht etwas was direkt sichtbar ist.

Behat + Mink: Selenium und eigene Methoden

Wenn man mit JavaScript zu tun bekommt, führt leider kein Weg an Selenium vorbei. Das erst einmal zum laufen zu bekommen ist dabei auch nicht immer einfach. Ich habe dabei gelernt, dass Firefox nicht wirklich funktioniert und Chrome dagegen ohne Probleme funktioniert. Um Selenium zum Laufen zu bekommen brauchen wir einiges:

- eine aktuelle Version des Selenium Standalone Servers als JAR
- den Chrome-Driver für Selenium
- Java

dann kann man Selenium mit java -Dwebdriver.chrome.driver=/opt/selenium/chromedriver -jar selenium-server-standalone-X.X.X.jar starten. Den Driver kann man natürlich auch in anderen Verzeichnissen ablegen.
https://selenium-release.storage.googleapis.com/index.html

In der Behat-config sieht es dann so aus:


default:
suites:
default:
contexts:
- Behat\MinkExtension\Context\MinkContext
# - TestContext
extensions:
Behat\MinkExtension:
base_url: 'http://www.hannespries.de'
files_path: 'vendor'
browser_name: 'chrome'
selenium2: ~


Um nun auch eigene Methoden wie Warten hinzuzufügen brauchen wir einen eigenen Context. Den legen wir unter features/bootstrap ab.


<?php

class TestContext extends \Behat\MinkExtension\Context\MinkContext {
/**
* @When I wait :arg1 seconds
*/
public function iWaitSeconds($seconds)
{
$this->getSession()->wait($seconds * 1000);
}

/**
* @When I click the :arg1 element accepting the alert
*/
public function iClickTheElementAcceptingTheAlert($selector)
{
$page = $this->getSession()->getPage();
$element = $page->find('css', $selector);

if (empty($element)) {
throw new Exception("No html element found for the selector ('$selector')");
}

$element->click();

$driver = $this->getMink()->getSession()->getDriver();
if($driver instanceof \Behat\Mink\Driver\Selenium2Driver){
/** $driver \Behat\Mink\Driver\Selenium2Driver*/
$driver->getWebDriverSession()->accept_alert();
}
}
}


Jetzt den Context eintragen. Weil wir mit extends arbeiten dürfen nicht beide aktiv sein, weil die sonst die selben Methoden implementieren.


default:
suites:
default:
contexts:
# - Behat\MinkExtension\Context\MinkContext
- TestContext
extensions:
Behat\MinkExtension:
base_url: 'http://www.hannespries.de'
files_path: 'vendor'
browser_name: 'chrome'
selenium2: ~


Nun können wir Warten und sogar Javascript Dialoge bestätigen.

Wenn wir nun mit "bin/behat features/XXX.feature" unser Test Scenario XXX starten öffnet sich der Chrome-Browser und wir können alle Tests visuell mit verfolgen.

bbcode-image

Orange Pi One: Ein passendes Netzteil

Leider kann man über den Orange Pi One nicht über den USB-Anschluss mit Strom versorgen sondern braucht ein 5V 2A Netzteil. Ich habe mir dieses Aukru 8 in 1 USB Ladegerät gekauft und mit den Adaptern passte es dann auch und der Adapter sitzt auch so, dass ich erst einmal den Karton als vorläufiges Gehäuse verwenden kann.

bbcode-image


bbcode-image


Damit stehen den Experimenten mit Motioneye OS nichts mehr im Weg. Wenn das nichts wird, kommt einfach RetroPi rauf :-)


Shopware: SEO-Urls selbst anlegen

Eigene SEO-Urls kann man mit Shopware relativ einfach hinzufügen, wenn man die technischen Urls kennt. So kann man auch einem Blog-Post eine zweite SEO-Url anlegen, die nicht direkt als Blog-Url erkennbar ist. Diese Urls sind keine Weiterleitungen mit 301 oder 302 sondern echte Urls, die den selben Content liefern, wie die originalen Urls, was bei SEO zu beachten ist.

bbcode-image


bbcode-image


bbcode-image

Java: JUnit Tests mit Mockito vereinfachen

Das Mocking von Services war lange eine sehr komplizierte Sache. Mit Javassist oder speziellen Implementierungen konnte man es lösen, aber es war oft einfach nicht schnell und einfach zu benutzen. Mit Mockito kann man pro Test die Method-Calls einer Klasse überschreiben und so relativ einfach auch Test für Klassen mit Dependency Injection schreiben. Wenn man eine Service-Klasse testen möchte, die auf ein DAO zurückgreift um Daten aus der Datenbank zu laden, kann man nun einfach die DAO-Klasse mit Mockito so manipulieren, dass sie bestimmte Daten liefert ohne auf die Datenbank zu gehen und so hat man die volle Kontrolle über die Input- als auch die Output-Daten.


import org.junit.*;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import static org.mockito.Mockito.*; //to use when() without static class

public class ModelTest {

//will always return a model with id = 0 (not existing), simulating an empty/not existing database
@Mock
private TestModelDAO dao;

@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();

@Before
public void setup(){
//creating an existing dummy for id 23, so 23 will always return a valid model
TestModel dummy = new TestModel();
dummy.setId(23);
dummy.setName("blubb-23");
when(this.dao.getModel(23)).thenReturn(dummy);
}

@Test
public void getModelSimple(){
TestModel model = (new TestModelDAO()).getModel(42);
Assert.assertEquals(0, model.getId());
}

@Test
public void getModelComplexId(){
TestModel model = this.dao.getModel(23);
Assert.assertEquals(23, model.getId());
}

@Test
public void getModelComplexName(){
TestModel model = this.dao.getModel(23);
Assert.assertEquals("blubb-23", model.getName());
}

@Test
public void getModelDIExample(){
//Testing a service with constructor dependency injection
TestService service = new TestService(this.dao);
String upperName = service.getUpperCaseName(23); //name for id 23
Assert.assertEquals("BLUBB-23", upperName);
}
}


An sich sollte man solche Methoden wie getUpperCaseName nie so schreiben und immer die fertig geladene Entität rein reichen. Aber gerade bei älteren Legacy-Code findet man solche Dinge oft. Auch Tests mit fehlenden Request oder ähnlichen kann man so durchführen, ohne direkt eine gesamte HTTP-Umgebung nachbauen zu müssen.



Python: Django vs Turbo Gears 2

Ich musste mich damit beschäftigen wie man eine kleine REST-API mit Python erstellt. Django macht dabei an sich bei jeden Pups bei mir Probleme und war doch sehr umständlich, weil man sich noch mit dem gesamten MVC-Pattern darin beschäftigen musste. Nachdem es auch mit dem ORM schwieriger wurde (im Vergleich zu Spring Boot mit JPA/Hibernate) dachte ich mir, es müsse doch auch für Python was modernes geben. So kam ich zu Turbo Gears 2 und das macht schon mal genau was ich wollte. Einfach, schnell und übersichtlich.


from tg import expose, TGController, AppConfig
import jsonpickle

# --

class TestEntity(object):
def __init__(self, id ,name, sub):
self.id = id
self.name = name
self.sub = sub

class TestSubEntity(object):
def __init__(self, value):
self.value = value

# --

class RootController(TGController):
@expose("json")
def index(self):
test = TestEntity(42, 'blubb', TestSubEntity('sub-blubb'))
return jsonpickle.encode(test, unpicklable=False)

@expose("json")
def method2(self):
test = TestEntity(23, 'blubb-2', TestSubEntity('sub-blubb-2'))
return jsonpickle.encode(test, unpicklable=False)


# --

config = AppConfig(minimal=True, root_controller=RootController())

application = config.make_wsgi_app()

from wsgiref.simple_server import make_server

print("Serving on port 8090...")
httpd = make_server('', 8090, application)
httpd.serve_forever()


Mit Turbo Gears 2, Spring Boot und Meecrowave kann wirklich schnell und einfach Microservices erstellen und vieles des Overheads alter Zeiten ist einfach nicht mehr da bzw wurde so gut versteckt, dass man sich rein auf den Code und die Logik konzentrieren kann. Welche Lösung man da nimmt ist Geschmackssache. Von der Codestruktur her sieht alles an sich fast 100% gleich aus.

Bei Python sehen Flask, Bottle und Hug auch interessant aus


Preload-Cache PHP: Wie Java nur eben schlechter.

Ich mag PHP. Ich mochte PHP schon zu PHP4- und PHP5-Zeiten, obwohl da Performance und Typisierung wirklich nicht immer so toll waren. Aber es gab einen großen Vorteil, im Vergleich zu Java (Tomcat + JSP/JSF/Servlets), den man konnte einfach eine Datei ändern und F5 drücken. Dann war die Änderung direkt zu sehen. Das Betraf auch nciht nur einfache Controller und Views sondern auch DB-Connections, Caches und andere Dinge bei denen man normaler Weise den Java Servlet-Controller hätte neustarten müssen. Dieser Luxus wurde sich eben dadurch erkauft, das es keinen Application-Scope oder komplexes Connection-Pooling in PHP gab. Aber dafür ersparte man sich nicht-funktionierende Hot-Deploys und Restart-Zeiten der Serverumgebung.

Nun soll es einen PHP Preload-Cache geben, wobei die Kompilierung des PHP-Codes nur noch beim Serverstart erfolgen soll: A server restart will be required to apply the changes. Damit werden dann einige (die ersten) Seitenaufrufe schneller, weil dann dort das Kompilieren entfällt. Bei Blogs oder one-page Lösungen sehe ich da keinen wirklichen Vorteil. Die Kompilierungszeiten mit PHP 7 sind wirklich schnell geworden, so das auch der erste Aufruf der 90% der Anwendung kompilieren würde bei dem normalen OpCache, gefühlt nur minimal langsamer wäre als die weiteren Aufrufe. Der absolute Nachteil wäre, das man den Server wegen jeder Änderung neustarten müsste. Also würde man sich Scripte schreiben, die das Starten und Kompilieren für einen übernehmen. Dann noch WebPack und man hat einen Workflow, der genau so komplex ist wie bei Java-Projekten (nur dass dort mit Maven alles zentraler geregelt wird).

Was habe ich also am Ende. Komplexität, Deployment-Zeiten, eine immer noch nicht ganz so überzeugende Performance, kein Multithreading, keinen Application-Scope und kein Connection-Pooling für Datenbankverbindungen. Also alles was an Java nicht so toll ist und keinen der Vorteile von Java.

Besonders, wenn man immer mehr in die Richtung geht einen HTML5+JS Client mit einer REST-API/Middleware anstelle klassischer MVC-Modelle zu nutzen, wären die F5-Vorteile von PHP wirklich toll gewesen.

Wer also sehr komplexe Seiten mit vielen vielen verschiedenen Views hat, der kann Vorteile durch einen Preload-Cache haben. Bei allem anderen sollte der OpCache allein noch die Performance bestimmen.

Behat und Mink: Einfach Websites und HTML-UIs testen

Unit-Tests sind ja immer ganz schön, um Berechnungen und andere atomare Logiken zu testen. Berechne ich den Preis richtig? Funktioniert das Regex- oder XPath-Pattern noch? Kann man alles mit Unit-Tests super und zu zuverlässig testen. Wenn es kann aber um Workflows oder Benutzerführung geht wird es schwer. Auch muss ein UX-Spezialist nach jeder neuen Version wieder testen, ob die von ihm festgelegten Wege und Regeln noch genau so funktionieren und der Benutzer auch das zusehen bekommt was er soll und nicht plötzlich in einer falschen Ansicht landet.

Hier kommt Mink ins Spiel. Mink wird verwendet von Behat und das ist ein Behavior Driven Development Framework. Wie auch bei Unit-Tests wird erst formuliert, was wo passieren soll und wie der Besucher sich auf der Seite bewegt und was er wann zu sehen bekommt.

Auf meinem Blog soll ein Besucher mit der Hauptansicht des Blogs starten und wenn er nach "rfid" sucht, den Post mit "POS-Plugin Cashless-Payment Demo" finden. Außerdem soll er beim PoE-Kameras Post Kommentare hinterlassen können.

Behat aufsetzen ist nicht immer ganz einfach, weil es beim Composer teilweise zu Versionsproblemen kommen kann. Deswegen hier mein kleines Grund-Setup.

composer.json

{
"name": "hp/behat_test",
"authors": [
{
"name": "Hannes Pries",
"email": "hp@hannespries.de"
}
],
"require": {
"behat/behat": "*",
"behat/mink-extension": "*",
"behat/mink-browserkit-driver": "*",
"behat/mink-goutte-driver": "*",
"behat/mink-selenium2-driver": "*"
},
"config": {
"bin-dir": "bin/"
}
}


Dazu kommt die behat.yml im Projekt-Root (Behat 3):

default:
suites:
default:
contexts:
- Behat\MinkExtension\Context\MinkContext
extensions:
Behat\MinkExtension:
base_url: 'https://www.hannespries.de'
files_path: 'vendor'
goutte: ~


und jetzt kommt der Test unter features/home.feature:

Feature: Home

Scenario: Starting with blog index page
Given I am on "/"
Then I should see "Latest Entries"

Scenario: Search for RFID
Given I am on "/index.php?page=Blogs&sub=search"
When I fill in "pattern" with "rfid"
And I press "search"
Then I should see "POS-Plugin Cashless-Payment Demo"

Scenario: Display comment-fields in blog-post
Given I am on "/idx-blog--berwachtungskameras-mit-poe-und-nas.html?page=Blogs&sub=viewBlog&blogId=465"
Then I should see "write comment:"

Scenario: Display comment-fields in blog-post 2
Given I am on "/idx-blog--berwachtungskameras-mit-poe-und-nas.html?page=Blogs&sub=viewBlog&blogId=465"
Then I should not see "Not able to write comment"


Und das war es auch schon. Jetzt kann man den Test über bin/behat features/home.feature starten und es wird alles einmal durch getestet.

bbcode-image


Eine gute Übersicht über die Syntax findet man hier https://gist.github.com/mnapoli/5848556.

Und eine Beispiel-Projekthier:
https://github.com/jaffamonkey/behat-3-kickstart

Allein mit dem wenigen Syntax den ich dort verwendet habe kommt man schon relativ weit. Wenn man stark JavaScript-basierte WebApps testet gibt entsprechende Erweiterungen, um auch direkt selbst mit JavaScript dinge triggern oder Events abfeuern zu können.

Geo-Distance Berechnung

Vor einiger Zeit hatte ich eine PLZ-Suche implementiert, bei der Locations bis zu einer bestimmten Distanz zu einer 2. per PLZ identifizierten Location als Ergebnismenge heraus gesucht wurden.

Es wurde also Also eine Distanz zwischen 2 per GPS-Koordinaten beschrieben Locations berechnet. Ich habe das in PHP erledigt, weil die Locations der zu findenen Orte in einer JSON-Datei vorlagen und ich mir den Import in die MySQL-DB ersparen wollte. Die Orte per PLZ kamen aber aus der MySQL-DB, wobei immer der erste Ort mit der PLZ die Koordinaten geliefert hat.


/**
* https://www.geodatasource.com/developers/php
*
* @param $lat1
* @param $lon1
* @param $lat2
* @param $lon2
* @param string $unit
* @return float
*/
public function calcDistance($lat1, $lon1, $lat2, $lon2, $unit = 'K'): float
{
$theta = $lon1 - $lon2;
$dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
$dist = acos($dist);
$dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515;
$unit = strtoupper($unit);

if ($unit == 'K') {
return ($miles * 1.609344);
} else if ($unit == 'N') {
return ($miles * 0.8684);
} else {
return $miles;
}
}


Auf der Seite findet man auch Code für PL/SQL (Oracle), Java und JavaScript. Nur leider für MySQL nicht. Die findet man aber hier. Im Grunde kann man sich das aber für jede Sprache selbst ableiten, weil es normale Mathematische Berechnungen sind ohne dass zusätzliche Libs oder Klassen nötig wären (die es aber auch gibt und diese Logik kapseln).

Jeden Falls sind solche Berechnungen an sich ganz einfach und performant, so dass man sich alles schnell selbst schreiben kann und nicht auf 3rd Party Lösungen angewiesen ist.

Überwachtungskameras mit PoE und NAS

Wenn man ein eigenes Haus hat, ist man dann doch irgendwann immer an dem Punkt, wo man sich über Sicherheit Gedanken macht. Dabei gibt es wie bei der IT zwei Stufen an Sicherheit.
Einmal Einbrüche verhindern und Versuche erkennen und aufzeichnen. Es ist zwar schön, wenn sie die Terrassentür nicht so einfach auf hebeln lässt, aber man will den Versuch ja trotzdem dokumentiert haben. Falls es jemanden doch gelingen sollte, will man auch dann Material haben, um alles nachweisen zu können und vielleicht sogar gutes Bildmaterial für die Strafverfolgung zu haben.

Dann kommen immer Überwachungskameras ins Spiel. Klar muss einem sein, dass so eine Kamera einen Einbruch nicht verhindert, sondern nur bei der Aufklärung helfen kann. Gute Fenster und Türen ersetzt eine Kamera nie. Die Kunst bei Überwachungskameras ist, diese so zu positionieren, dass sie nur Stellen aufzeichnen, die nicht andere Personen und deren Persönlichkeitsfreiraum einschränken und doch genau Raum abdecken, um viele und gute Bilder oder Videos zu machen.

Indoor ist meistens nicht nötig und vielleicht nur in abgetrennten Eingangsbereichen sinnvoll. Abgetrennt ist, wichtig wenn man Katzen oder Hunde hat, weil sonst würden, diese wohl die Kamera zu oft auslösen. Sensible Bereiche will man ja selbst auch gar nicht überwachen.

Die Bilder und Video sollen nicht nur in der Kamera gespeichert werden, sondern in Echtzeit an einen sicheren FTP-Server gesendet werden. Sicher bedeutet hierbei, dass der Server bei einem Einbruch selbst nicht geklaut oder zerstört werden kann. Cloud-Lösungen sind hier teilweise nicht verkehrt. Ein kleines QNAP-NAS lässt sich aber auch gut versteckt im Haus unterbringen. Das Speichern auf einem FTP in Echtzeit ist wichtig, dass auch wenn jemand die Kamera beschädigt oder klaut, hierbei wenigstens so lang wie möglich Bilder oder Videos erstellt und gesichert werden. Eine Kamera nur mit SD-Karte, die geklaut werden kann ist an sich sinnlos.

Es gibt schon gute und günstige Outdoor IP-Kameras. Ob man WLAN braucht oder Netzwerkkabel legen will muss jeder selbst wissen. ABER da Kabel (ausfall-)sicherer sind und man diese gleichzeitig zur Stromversorgung über PoE verwenden kann, habe ich mich bei mir rein für eine LAN-Kabel gestützte Lösungen entschieden, auch wenn eine der Kameras WLAN hat.

bbcode-image


bbcode-image


Neben einem 1GBit Switch kommt noch ein weiterer 100Mbit PoE Switch aus China zum Einsatz. 1GBit habe ich da hauptsächlich, wegen dem SAT DLNA-Server, der über die selben Leitungen geht, aber allein für Überwachungskameras würde 100MBit ausreichen.

Wichtig ist die Auflösung der Kamera. 720p klingt besser als es ist. 1080p ist schon sehr viel besser und ich würde nur für relativ unwichtige Bereiche oder welche wo die Menschen dicht an der Kamera vorbei gehen müssen noch mit 720p ausstatten. 1080p mit Infrarot bekommt es meistens gut hin auch Nummernschilder gut lesbar abzubilden. 720p hat hier schon schnell Probleme.

bbcode-image

Meine 720p Kamera (mit WLAN)

bbcode-image

Meine 1080p Kamera (nur LAN)

Bilder und Videos landen bei mir auf meinem kleinen QNAP TS-120 NAS. Eine externe 500GB eSATA HDD kann schon viele viele Tage halten, wenn man wirklich nur bestimmte Bereiche per Motion-Detection überwacht, an denen nicht im Minutentakt Menschen oder Vögel vorbei gehen/fliegen. Wind + Büsche sollten auch nicht in dem Bereich zu finden sein,weil die doch die Motion-Detection sehr effektiv auslösen.
Spinnen direkt vor der Kamera können ein Problem sein. Aber meistens bauen die 2-3 Abende ihr Netz und stören dann nicht weiter.

Bis hier hin ist alles toll und funktioniert super. Aber es gibt immer noch 2 Bereiche, wo ich mir unsicher bin wie ich das weiter handhaben soll. Einmal eine sehr gut versteckte Kamera, die das NAS überwachen soll. FTP + SD-Karte, damit sich Kamera und NAS gegenseitig absichern können. Gut getarnte Kamera mit Motion-Detection und FTP-Upload sind schwer zu finden, wenn man nicht unendlich viel Geld dafür ausgeben will.
Als zweites ist noch ein Fenster nicht überwacht, dass aber direkt zur Straße geht und wo es schwer ist eine Kamera mit Sicht auf das Fenster anzubringen. Ich mag Kamera an Außenwänden nicht besonders, aber da wird es wohl keine andere Lösung geben. Da wäre eine hübsche Kamera wie diese ganz gut, die auch etwas nach einer Lampe aussieht, damit sich niemand davon belästigt fühlt, auch wenn die Kamera gar nicht die Straße und andere Bereiche überwacht.

bbcode-image

Older posts:

Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?