[{"content":"Sometimes you have to configure stuff in Home Assistant the \u0026ldquo;painful\u0026rdquo; way: Working with Yaml-Files instead of using the UI. Therefore Studio Code Server is the first choice for most of us. But Yaml isn\u0026rsquo;t fun. Yaml in hass.io especially isn\u0026rsquo;t fun.\nBut there is ai for the rescue. AI isn\u0026rsquo;t always the solution, but working with ugly configuration formats is exactly where it is good at. So let\u0026rsquo;s add claude to the Studio Code Server\nStudio Code Server I assume you already have installed the Studio Code Server. If you haven\u0026rsquo;t: It is a simple Add-On. Just follow the official instructions: https://github.com/hassio-addons/addon-vscode/blob/main/vscode/DOCS.md\nStudio Code Server will then be installed as a docker container. (Don\u0026rsquo;t worry. No need to know anything about docker. This works in the background)\nPreparing Claude needs NodeJs which isn\u0026rsquo;t in the studio code server docker. But don\u0026rsquo;t worry, this is easy to solve. You could connect to the docker and install the required packages using app-get. But while this works this has two disadvantages. First it is a bit more complicated if you are not at home with Linux Systems and most important: Any update of the Addon would kill NodeJs and you would have to do this all over again.\nThankfully the Add-On devs thought of an easy way to do that for you. Just go to Settings - Add-Ons - Studio Code Server. There go to Configuration and in the \u0026ldquo;packages\u0026rdquo; enter \u0026ldquo;npm\u0026rdquo; and then \u0026ldquo;nodejs\u0026rdquo;. Do not enter both at the time. Each one of them by enter. After that there should be two bubbles on top:\nIf you prefer editing in yaml, here is your content:\npackages: - nodejs - npm init_commands: [] Save and restart the addon. Don\u0026rsquo;t be alarmed if this takes a LONG time. There will have big packages to be downloaded and installed into that container. This will take minutes even on a fast machine and especially if using a Raspberry Pi.\nAdding Claude Now you can switch to the code server. Select Extensions on the left side, enter \u0026ldquo;Claude\u0026rdquo; into the search and install \u0026ldquo;Claude Code for VS Code\u0026rdquo;. After that you should have that typical \u0026ldquo;Claude Star\u0026rdquo; on the top right corner. You will be greeted with the authentication method. Select the one that matches your account. (Most likely Claude.ai Subscription)\nIf after using the Login clicking \u0026ldquo;Open\u0026rdquo; does not work because you get redirected to a local url use the url instead that is shown in the textbox:\nJust copy paste that url manually and paste the returning code into Studio Code.\nUsing Claude Now you can open Claude using the star and let him do the work\nThat is it. Now you can use claude in Home Assistant Config Files\n","date":"2026-02-04T18:17:00+01:00","image":"/blog/use-claude-ai-in-home-assistant-studio/2026-02-04-10_37_55-Home-Assistant-Mozilla-Firefox_hu_9daa82fb4ffbfdf5.png","permalink":"/blog/use-claude-ai-in-home-assistant-studio/","title":"Use Claude AI in Home Assistant Studio"},{"content":"Before I begin to even start writing about this topic I assume it makes sense to mention that this article has not been created using AI. Not a single word. (with one exception: The german version has been translated automatically using Claude, like all entries on my blog) I also tried to write this for a wider audience than my usual articles. As this isn\u0026rsquo;t a specific problem solving for a weird use case in c# I tried to keep the tech gibberish down and especially do not expect you to have C# knowledge. Any coding experience helps, but isn\u0026rsquo;t required. On the other hand if you are a skilled dev, don\u0026rsquo;t be annoyed if I explain things trivial in your opinion; Just ignore the explanations and feel smart :)\nRuleset I simply wanted to test how good vibe coding (Letting the AI create your whole application) is today. Does it achieve what I want? Is the code quality good? Would it help someone who has no knowledge at all? So I set up a ruleset:\nMake sure that what I want to achieve hasn\u0026rsquo;t be done before (so the AI can\u0026rsquo;t just copy/paste a complete application) DO NOT CODE: Really. I try to not change a single character in a source file even if a mistake is obvious Try not to help: Of course if something is wrong, I will tell the AI. But let it find out how to solve this, first only helping if it got really stuck Application should not be too ease (no \u0026ldquo;Hello World\u0026rdquo;) Application should not be too complecated either (no \u0026ldquo;Please create a better Microsoft Teams application\u0026rdquo;) No external tools needed; So no Unity or Photoshop requirements, etc. I should not need to leave the IDE (Rider) No Security related stuff: No Password hashing, not storing important secrets, simply because I trust AI enough to do this lightly. This might be worth another experiment on its own Environment and Job description I used Rider with Claude AI integration using Sonnet 4. Whenever it asked for a permission I granted it. I started by giving a simple task and iterate over time. The task was:\nCreate a Razor Application that offers a SCRUM Poker functionality with a twist: It should have Joker Mechanics like in Balatro. The PO should be able to create a new game. Then an admin view and a player view should be created. Each of them by generating a random code like play?code=dbddhkp and admin?code=ukule2 . No database. Just store everything in memory\nI was sure this hasn\u0026rsquo;t been done before because it absolutely makes no sense. SCRUM poker is a tool to determine the \u0026ldquo;cost\u0026rdquo; (or complexity) of programming tasks. Each developer decides how complex the task at hand is usually using values from 1 to 21 (as always, there are a lot of variants with other numbers) and at the end these values gets compared and discussed. The main reason is that the number itself isn\u0026rsquo;t communicated openly at the beginning so if a senior dev tells his number other devs might just say \u0026ldquo;yepp. About the same\u0026rdquo;. The \u0026ldquo;poker\u0026rdquo; element makes sure everyone says the number he or she really thinks of and afterwards any discrepancies gets discussed.\nBalatro on the other hand is a fun Poker game. There are Jokers that change the value of the cards. E.G a \u0026ldquo;every heart gets multiplied by 4\u0026rdquo;. Obviously this would destroy any useful results that the SCRUM poker produced. So a nonsens project, a quite complex one nonetheless.\nI added additional features lateron like a multi-language feature but did want to give the AI the chance to iterate\nDid it work? The short answer is: Yes. I did not write a single line of code and the application worked.\nBut the long answer is: Developers wont loose their job very soon. As I will show in the learnings it requires a skilled developer to lead into the right direction when problems need to be solved. In addition some decision lead to bad code. The detailed pitfalls and also positive surprises can be found in Learnings.\n(And also I published the code on GitHub so you can see for yourself)\nThe biggest benefit: I am a backend developer. My programs (usually) work, but I am not really good in UI or UX. That benefit the AI was able to solve. The application wasn\u0026rsquo;t the most beautiful thing ever to be seen in a browser window; Still: It looked way better than anything I would have created\nSources and demonstration You can find the sources on the FiboNuts Repository on Github. A working example can be found at Poker.Oles.Cloud\nLearnings Code quality Code quality started good, but got worse, completely wrong assumptions leading to bad code. Forcing Claude to revert these things helped putting the code back in good shape. TBH I expected way worse\nHelp needed Quite at the beginning of the project I ran into a problem that could not be solved by the AI itself: There are URLs generated for the players. But when I enter those in a different browser (or even tab) the code does not get recognized. I tried to tell Claude in about 10 Iterations that it does not work. The AI did try different approaches but never came near a solution. After I told him that there is a problem in the Dependency Injection implementation and the class holding the state should not be added Scoped but added as a Singleton it was able to continue. But without my help this would be the end of the project.\nIt was surprising to see that such quite simple cause could not be recognized while much more complex issues got resolved.\nAlso at one point the communication between the server and client did not work and it seems like Claude simply could not believe when I said that. I had to offer assistance telling him to create logs that I can then paste back so Claude can analyze the issue. Adding random stuff Claude also decided at one point to add https. (It was http before) when I was asking to add a multi language feature. I assume he learned implementing that feature from a site where https was used. The https-part completly broke the application because ports had been assigned multiple times in the config.\nAlso until the very end there always was an ugly \u0026ldquo;About\u0026rdquo; link on the upper part of the page linking to https://learn.microsoft.com/en-us/aspnet/core/?view=aspnetcore-9.0. I assume this is part of any \u0026ldquo;how to create a Blazor Application\u0026rdquo; Tutorial and so Claude thought this is required\nWrong assumptions At one point I asked the AI to add a multi language feature using best practices. While it did that correctly it forgot to add a crucial Package from NuGet. The Build errors have been quite obvious stating \u0026lsquo;Are you missing a reference\u0026rsquo; but Claude did not seem to understand that. Instead it assumed that the code generated was too complex for the compiler (it absolutely wasn\u0026rsquo;t: Just a few lines adding language-strings to an array) and simply removed its code over and over again until the program compiled again but also had no language code left. Again I had to help and tell him: \u0026ldquo;Hey. That wasn\u0026rsquo;t the issue. You forgot some NuGet Packages\u0026rdquo;. After that the issue was resolved.\nAt some other point Claude managed to break the application completely. Right at the start it simply would not load anymore after the progress bar was finished. Claude did an assumption no dev would do: It was sure the cause was the logging. Too many log entries causing the application to slow down. So any log in any exception handling or anywhere else got removed. Not solving the issue, obviously but creating a way worse variant than before.\nAs a rule of thumb: Whenever the Bots writes something like \u0026ldquo;this is a known issue\u0026rdquo; it probably found some very weird edge case that has nothing to do with a problem that should be simple. In my case multi language not working caused really weird actions: Creating dlls for each language, trying to use reflection to fix some (non-existing) issue of the localizer when at the end the text in the razor page was simply static. After I told that there should be variables or method calls instead of fixed strings Claude was able to revert all the nonsense changes, but without that help this was unsolvable.\nWhat is considered easy? Tasks that I see as quite complicated have been solved sometimes without any issues while others, that shouldn\u0026rsquo;t be too hard seemed to be unsolvable. For example I wanted a \u0026lsquo;/metrics\u0026rsquo; endpoint to be used with Prometheus (A tool to collect metrics and display them in a Dashboard). While it looked right, it was a html page that rendered the content. Claude did never manage to follow the simple task: \u0026ldquo;please output plain text. No html tags.\u0026rdquo; Even when providing the correct Mimetype it still did not understand correctly. An additional Server application was created where the clients sent metrics, suddenly a IIS Express configuration appeared in the launchsettings and other things that made the program way, way worse but did not even move an inch into the right direction.\nAfter that, Claude was so confused that not even rolling back worked. The AI wasn\u0026rsquo;t able to find its files anymore and even tried to find it in regions of my computer where it isn\u0026rsquo;t supposed to look: Needless to say that these operation have been costly: After a few retries I had to surrender. Claude was never able to create a simple output in text/plain. Even if I gave example snippets. It always rendered HTML that looked like Text but still had html tags.\nInteresting tactics After breaking the application when adding https claude did some interesting things. It did not simply analyze the code but was sure it wasn\u0026rsquo;t its fault :) Instead it assumed there must be some other application causing the issue and so started nmap to analyze what other services on my machine might be to blame. It did not expect it to \u0026ldquo;break out\u0026rdquo; of my IDE. A reminder to not just allow everything but have a close look on all actions. I would not be surprised if it had decided to simply kill that other process if it really had been in external issue. (But the implementation explicitly asks for your permission whenever it does something it hasn\u0026rsquo;t before)\nConclusion/Opinion Useful, but needs to be monitored closely. And you must be aware that the code isn\u0026rsquo;t really \u0026ldquo;good\u0026rdquo;, just working. Just a minor issue in this application but when customer data is processed and stored bad design decisions can lead to big problems. There are already a lot of vibe coded applications, used by millions that are just really badly written and leak data all the time. Just one example of many: https://cybernews.com/security/ai-girlfriend-app-leak-exposes-400k-users/\nNo developer wants to be responsible for a catastrophe like this even before thinking of GDPR fines.\nAlso: Even more like in human developing: Use Source Control. Claude can create git commits without any problems. This helps rolling back to previous state if the AI created a mess after an error. But I would suggest to only use a local repository and push manually to be able to make sure no secrets end up in the code. Also don\u0026rsquo;t just check the current code but any commit if there was some secret at any time.\nNonetheless we also can\u0026rsquo;t simply ignore AI when creating programs. Even if the AI bubble at some day bursts the tools will remain. LLMs will still be there and be just any other tool on the Cloud or maybe even on local machines. No matter if someone makes big cash selling AI tools or not. So in my opinion a senior dev has to decide where it works and where it does not. Also because of the nature of AI : I will get better, but there will always be the same kind of errors you can\u0026rsquo;t get rid of. Hallucinations are a thing and always will be.\nI also think the next generation(s) will be the ones that benefit the most but also suffer the most because of AI. Benefit because it never was so easy to start. Need something done in your OS? Ask the AI to write a python script. Having trouble about Dependy Injection? The AI will provide a document with good examples. You could find that without AI, but that is just simpler and faster. On the negative side: Your problem gets solved, but maybe not in the best way. And this isn\u0026rsquo;t just an academic argument: You have to understand why we do not store passwords but use hashes instead to understand the underlying risk.\nAnd in addition from now on not much more content will be created. You get every information you need through AI. But when no new information is created on the internet in a few years AI will mostly scrape content it created itself leading to a perfect endless confirmation bias loop.\nAnd on a personal note: On the one hand: AI is a great tool, on the other hand it nearly already destroyed any use of searching the web. Have you ever tried to find information on \u0026ldquo;Is it ok if my dog eats stuff\u0026rdquo;? The first two search pages mostly contain AI generated content. And these range from \u0026ldquo;No problem. stuff is even very healthy\u0026rdquo; to \u0026ldquo;Never ever! One bite and your dog is dead!\u0026rdquo;. I got around this by using DuckDuckGo and the \u0026ldquo;Block all results from this site\u0026rdquo; - feature for a while but am at a point where sites with random domains get created faster than I cen add them to the list.\nMaybe I should use AI to keep these lists up to date\u0026hellip;. 😀\n","date":"2025-10-18T07:48:00+02:00","image":"/blog/vibe-coding-an-experiment/Screenshot-from-2025-10-18-15-54-54_hu_2d4d42bb98978604.png","permalink":"/blog/vibe-coding-an-experiment/","title":"Vibe Coding: An experiment"},{"content":"Grafana is a very useful tool for monitoring your own programs, discovering silly bugs, or simply looking at beautiful graphs :)\nAnother feature is \u0026ldquo;Alerts\u0026rdquo;. These can be configured arbitrarily, whether high CPU load, too many errors in the log, or similar should trigger an alarm.\nThis post won\u0026rsquo;t be about the alerts themselves, but rather about sending already configured alerts to Mastodon. These are called Contact Points in Grafana and initially don\u0026rsquo;t offer Mastodon integration. However, this can be worked around with a few simple steps.\nMastodon First, we need some preparation, because of course not just any random application can post to Mastodon under our name (thankfully).\nTherefore, we log into the instance of our choice, select Settings and then Development. This should look something like this:\n(If the list is empty, it means you haven\u0026rsquo;t created any applications yet. If it\u0026rsquo;s not empty, you don\u0026rsquo;t need this part of the tutorial at all, because you\u0026rsquo;ve already done this before 😀)\nClick on New Application here and choose a name. Leave the other two text fields as they are:\nFor permissions, you only need \u0026ldquo;write:statuses\u0026rdquo;.\nYou should now see a page showing the secrets. You only need the access token (bottom value) from this.\nGrafana Now we can switch to Grafana. There\u0026rsquo;s a menu item called Contact points under Alerting. At least Email should already appear in the list. We now add a new destination with \u0026quot;+ Create contact point\u0026quot;:\nThe name is of course freely selectable, for integration choose Webhook\nFor the URL, enter the following:\nhttps://chaos.social/api/v1/statuses?access_token=\u0026lt;Secret\u0026gt;\nInstead of , enter the access token from Mastodon. If you now click on Test (and leave the default values as they are), a new entry will already be created in Mastodon under your name: Getting Down to Business That\u0026rsquo;s nice for starters, but we obviously can\u0026rsquo;t do much with just \u0026ldquo;firing\u0026rdquo;. We want to see the actual error. Also, we probably don\u0026rsquo;t want to inform the whole world. It makes more sense to simply mention ourselves and set the visibility to \u0026ldquo;direct\u0026rdquo;.\nTo do this, we open Optional Webhook Settings, set HTTP Method to POST and click the \u0026ldquo;Edit Payload Template\u0026rdquo; button next to Custom Payload. A new window pops up and after we\u0026rsquo;ve selected Enter custom payload template there, we can enter the following content:\n{ \u0026#34;status\u0026#34;: \u0026#34;Alert: {{ .CommonLabels.alertname }}\\nStatus: {{ .Status }}\\n{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}\u0026#34; } Now the alert is already a bit more meaningful: Still missing the private mode. This can be set via Visibility. Additionally, it\u0026rsquo;s best to mention yourself. (I\u0026rsquo;ve modified my name slightly here so you don\u0026rsquo;t make a copy-paste mistake :) )\nWhile we\u0026rsquo;re at it, I\u0026rsquo;ve also used the spoiler-text.\n{ \u0026#34;status\u0026#34;: \u0026#34;@guacamoleeeee {{ .CommonAnnotations.summary }}\u0026#34;, \u0026#34;visibility\u0026#34;: \u0026#34;direct\u0026#34;, \u0026#34;sensitive\u0026#34;: false, \u0026#34;spoiler_text\u0026#34;: \u0026#34;🚨 {{ .CommonLabels.alertname }} {{ .Status }}\u0026#34;, \u0026#34;language\u0026#34;: \u0026#34;en\u0026#34; } The result is then as follows: A complete list of Mastodon properties can be found here: https://docs.joinmastodon.org/methods/statuses/\nHave Fun Wishing you many alerts might be a bit strange, but I hope I could help with this.\n","date":"2025-10-14T12:28:00+02:00","image":"/blog/grafana-notifications-for-mastodon/Screenshot-from-2025-10-14-12-21-45_hu_6d1258580808eff7.png","permalink":"/blog/grafana-notifications-for-mastodon/","title":"Grafana - Notifications for Mastodon"},{"content":"3D printing can be fun, but can also be very annoying if something bad happens. So I wanted to have an easy indicator if something goes wrong without having to check the print every few minutes\nWLED to the rescue My 3D - Printer is in a small room. I usually do not have to enter that room until I want to remove the print. So I decided to add an RGB-LED-Strip that shows how the print is running. I decided to use WLED using a cheap ESP8266MOD. Installing the WLED-Software is easy. It can even be done using a simple browser if you are on Windows or Mac: https://install.wled.me/\nHome Assistant After that I needed to write some automations. The WLED is detected automatically on Home Assistant, for my Creality 3d Printer I used the \u0026ldquo;Moonraker\u0026rdquo; Integration available via HACS: https://moonraker-home-assistant.readthedocs.io/en/stable/index.html\nError display The printer reports its Print state through the \u0026ldquo;sensor.k2plus_b4f8_current_print_state\u0026rdquo; (yours can have a different prefix). The following values are possible: standby, printing, paused, complete, cancelled, error But errors are never reported as an \u0026ldquo;error\u0026rdquo; but always as \u0026ldquo;paused\u0026rdquo;. There might be more critical ones that are reported as error but I never had those. So all we have to do is to react on the \u0026ldquo;paused\u0026rdquo; - state and set the WLED accordingly:\nalias: 3DP Error description: \u0026#34;\u0026#34; triggers: - entity_id: - sensor.k2plus_b4f8_current_print_state trigger: state to: paused conditions: [] actions: - target: entity_id: light.wled_2 action: light.turn_on data: effect: Breathe rgb_color: - 204 - 0 - 0 mode: single Again: Your Entities might have different names\nProgress display To be able to display the progress we need the progress-Entity and in addition make sure that the printer really is printing:\nalias: 3DP Progress description: \u0026gt;- Updates WLED2 fill level to match sensor.k2plus_b4f8_progress value with purple color triggers: - entity_id: sensor.k2plus_b4f8_progress trigger: state conditions: - condition: state entity_id: sensor.k2plus_b4f8_current_print_state state: printing actions: - target: entity_id: light.wled_2 data: rgb_color: - 128 - 0 - 128 effect: Percent action: light.turn_on - target: entity_id: number.wled_intensitat_2 data: value: \u0026gt; {% set progress_value = states(\u0026#39;sensor.k2plus_b4f8_progress\u0026#39;) | float(0) %} {% if progress_value \u0026gt; 100 %} 100 {% elif progress_value \u0026lt; 0 %} 0 {% else %} {{ progress_value | round(1) }} {% endif %} action: number.set_value Display Countdown Last but not least: Because the 3d printer consumes quite an amount of power even on standby I want to shutdown power automatically when he finished printing. Shutting down itself is quite simple:\nalias: 3DP Strom automatisch aus description: \u0026#34;\u0026#34; triggers: - trigger: state entity_id: - sensor.k2plus_b4f8_current_print_state to: complete for: hours: 0 minutes: 15 seconds: 0 conditions: - condition: numeric_state entity_id: sensor.k2plus_b4f8_hotend_fan below: 10 - condition: numeric_state entity_id: sensor.k2plus_b4f8_extruder_temperature below: 50 actions: - type: turn_off device_id: 710995d98fe3640e134ae275e94dc491 entity_id: 7cd829033182474ac8f91a7c11539dfa domain: switch mode: single I wait 15 minutes and just to be sure I make sure the temperature of the extruder has gone below 50 degrees\nFinally show the countdown for that 15 minutes:\nalias: 3DP CountDown for Shutdown description: \u0026#34;\u0026#34; triggers: - trigger: state entity_id: - sensor.k2plus_b4f8_current_print_state to: complete for: hours: 0 minutes: 0 seconds: 10 conditions: [] actions: - target: entity_id: light.wled_2 data: rgb_color: - 0 - 255 - 0 effect: Percent action: light.turn_on - target: entity_id: number.wled_intensitat_2 data: value: 100 action: number.set_value - repeat: count: 60 sequence: - delay: hours: 0 minutes: 0 seconds: 15 milliseconds: 0 - target: entity_id: number.wled_intensitat_2 data: value: \u0026gt; {% set calculated_value = 100 - ((repeat.index + 1) * (100/60)) %} {% if calculated_value \u0026lt; 0 %} 0 {% else %} {{ calculated_value | round(1) }} {% endif %} action: number.set_value mode: single That\u0026rsquo;s it. These are my automations for my 3D printer status\n","date":"2025-05-06T14:02:00+02:00","permalink":"/blog/led-progressbar-for-3d-printing/","title":"Led Progressbar for 3d Printing"},{"content":"Installing DaVinci Resolve on Linux Mint takes a few steps. But at the end it simply works after you have done those.\nI tested those on Linux Mint but should work on most Ubuntu - based Distros.\nI mostly followed the instructions from virtual curiosities and just added a few of my own additions\nDownloading DaVinci Resolve The first step is to download DaVinci resolve from the official website. You can chose between the free version and the paid version. I really think the paid version is worth the price but the free version nearly allows anything you will need. And of course for testing it should be the first way to go.\nAfter you have downloaded the zip, extract it and open the console on that folder. Make sure the .run- file can be executed:\nchmod +x Davinci_Resolve_[version]_Linux.run (replace version with the version you downloaded. There should be only one .run - file) now run the installer: ./Davinci_Resolve_[version]_Linux.run You will get error messages about missing dependencies. Don\u0026rsquo;t worry. This is to be expected. Use apt install to install all the missing dependencies that are thrown at you e.g.\nsudo apt install libasound2 libglib2.0-0 There will be way more dependencies missing. After you have done that restart the installer. Now the next bunch of missing dependencies are shown. Repeat that process until no new dependencies are shown. And yes: This will take up to ten times or so. This does not mean you are in an endless loop. At some point this will stop.\nYou will most likely find a missing dependency for libasound2 which cannot be installed on Mint. Use libasound2t64 instead.\nOnce the installer only shows missing dependencies you already installed you can disable the dependencycheck: export SKIP_PACKAGE_CHECK=1 and rerun the installer\nNow finally the Wizard should show up and DaVinci gets installed. It will take a while until this is finished.\nAfter the installation you should create a shellscript to run DaVinci (the icon on the desktop created does not work from start): env LD_PRELOAD=/lib/x86_64-linux-gnu/libglib-2.0.so.0:/lib/x86_64-linux-gnu/libgio-2.0.so.0:/lib/x86_64-linux-gnu/libgdk_pixbuf-2.0.so.0:/lib/x86_64-linux-gnu/libgmodule-2.0.so.0 /opt/resolve/bin/resolve %u call the file davinci.sh and make sure it can be run: chmod +x ./davinci.sh Now you should be able to start DaVinci with ./davinci.sh\nOnce you are in DaVinci Resolve you will notice that the first Videos you try to add as media will not display. And the few that do most likely will not have audio. The reason is that on Linux DaVinci is quite picky what Video formats to accept.\nUse Piero for the rescue.\nThis tool automatically created Videos that can be read by Resolve and also creates smaller Proxy files. You will need to install dotnet 8 runtime for this to work\nAnd that`s it. You should be able to work with Resolve on Linux, now.\n","date":"2025-01-25T12:00:00+01:00","permalink":"/blog/davinci-resolve-on-linux-mint/","title":"Make Videos work with DaVinci Resolve on Linux Mint"},{"content":"To be completely honest: I do not really like automatic translations. Especially if you are not born in an english-speaking country, translations have been a real pain from the beginning. MSDN was one of those contenders that did really, really bad translations, years before AI was something to talk about.\nNow with AI the translations get better, but there are still bad ones and too many contents get translated. YouTube is nearly not usable anymore from germany if you are interested in original content.\nWhat way to go? Having said that I still think a translation can be something really useful. First of all it has to be good. And second requirement is that users have a choice.\nSo I decided to add translations to my blogpost. I stopped blogging in two languages in 2019, so all german entries have been very old. Time to check if there are good solutions out there.\nBecause I translated a book I already had some comparisons. At least from german to english currently my favorite tool was Claude.io. I did not really like the quality of DeepL and ChatGPT had good results but was very slow. In addition: Claude really seems to like MarkDown. Which is helpful in this case.\nThis time I wanted to use the API to automatically translate my Grav-Blogitems. Bad news is: You need a paid account for that. Good news is: It does not really matter how many money you use to fill your wallet and it is quite cheap.\nMy 50KB of data made up about 20 Cents. Of course your milage may vary. This is not sponsored, BTW.\nPreparations After creating an account, you can use the NuGet-Package Anthrophic.SDK. You simply create a new Client with your secret and can start with some messages. If you ever worked with OpenAi-API or Llama creating the message is nearly the same:\nvar client = new AnthropicClient(new APIAuthentication(_apiSecret)); var messages = new List\u0026lt;Message\u0026gt;() { new Message(RoleType.User, \u0026#34;Bitte übersetze das folgende Markdown nach Deutsch.\u0026#34;); ), new Message(RoleType.User, markdown) }; In case you are curious the message simply tells to translate a markdown into german\nThen you add the parameters. Temperature is - as known for AI-APIS how \u0026ldquo;creative\u0026rdquo; the AI is allowed to get. MaxTokens is a required value and can\u0026rsquo;t be too high:\nvar pars = new MessageParameters() { Messages = messages, MaxTokens = 1024, Model = AnthropicModels.Claude35Sonnet, Stream = false, Temperature = 1m, }; `Getting the response now is easy:` var response = await client.Messages.GetClaudeMessageAsync(pars); What I don\u0026rsquo;t like: If anything is wrong with your account you do not really receive a useful error code but simply null I had this problem before putting some money into the account.\nThe response is now in response.Message. The result is great, but long texts just stop after a while. Response.StopReason tells us why:\nmax_tokens\nThe obvious solution would be to increase the allowed tokens but as I said MaxTokens has a limited range. So you have to catch that error and go into a loop. Fun thing is: Claude \u0026ldquo;forgets\u0026rdquo; what he said before. So you have to tell him and ask to continue afterwards:\nwhile (response.StopReason == \u0026#34;max_tokens\u0026#34;) { pars.Messages.Add(new Message(RoleType.Assistant, response.Message)); pars.Messages.Add(new Message(RoleType.User, \u0026#34;bitte fahre fort\u0026#34;)); response = await client.Messages.GetClaudeMessageAsync(pars); } That`s it. Not really that complicated after all.\nAI doing AI things While the result is quite good I had a few issues. Being a coder it is a bit of a learning curve that errors cannot be fixed through coding but only by messages. The first was that the translation is quite formal. An additional message changed that: pars.Messages.Add(new Message(RoleType.User, \u0026quot;Enthält der Text eine persönliche Anrede, verwende 'Du' und nicht 'Sie'. \u0026quot;); (I will not be able to translate this command. It is a really german thing :) )\nAnother issue is that Claude likes to explain things. So we have to tell him to stop doing that:\npars.Messages.Add(new Message(RoleType.User, \u0026quot;Antworte ausschließlich mit dem Dokument, ergänze keine Erklärungen. \u0026quot;); (Which translates into \u0026lsquo;Only reply with the document, do not explain anything\u0026rsquo;)\nSurprisingly I had to add no additional message to not translate code. If you translate apt-get install into german apt-get installiere for example that would not work. I saw this happen on other sites but Claude did not need any additional hints.\nOn the other hand. Claude sometimes decided that my code wasn\u0026rsquo;t interesting enough:\n[Rest des Dokuments enthält die gleichen Code-Blöcke und technischen Anweisungen wie im Original, nur der erklärende Text wurde ins Deutsche übersetzt]\nWhich roughly translates into \u0026ldquo;the rest of the document contains the same code as the original. Did not translate that\u0026rdquo;. Did not expect that, but that was fixable as well: pars.Messages.Add(new Message(RoleType.User, \u0026quot;Fasse keine Inhalte zusammen, sondern gebe alles aus. Ziel soll sein dass man das deutschsprachige Dokument genauso alleine versteht wie das englischsprachige \u0026quot;): which roughly translates into \u0026ldquo;Do not summarize contents. The goal should be that the german document can stand on its own\u0026rdquo;\nNow the result is exactly as needed. For completionists sake here is the complete code:\nprivate async Task\u0026lt;string\u0026gt; TranslateSingleFile(string source) { var translated=string.Empty; var markdown = File.ReadAllText(source); var client = new AnthropicClient(new APIAuthentication(_apiSecret)); var messages = new List\u0026lt;Message\u0026gt;() { new Message(RoleType.User, \u0026#34;Bitte übersetze das folgende Markdown nach Deutsch.\u0026#34; +\u0026#34; Antworte ausschließlich mit dem Dokument, ergänze keine Erklärungen. \u0026#34; + \u0026#34;Enthält der Text eine persönliche Anrede, verwende \u0026#39;Du\u0026#39; und nicht \u0026#39;Sie\u0026#39;. \u0026#34; +\u0026#34;Fasse keine Inhalte zusammen, sondern gebe alles aus. Ziel soll sein dass man das deutschsprachige Dokument genauso alleine versteht wie das englischsprachige \u0026#34; ), new Message(RoleType.User, markdown) }; var pars = new MessageParameters() { Messages = messages, MaxTokens = 1024, Model = AnthropicModels.Claude35Sonnet, Stream = false, Temperature = 1m, }; var response = await client.Messages.GetClaudeMessageAsync(pars); if (response?.Message == null) throw new Exception(\u0026#34;No valid response from Claude\u0026#34;); translated += response.Message.ToString(); while (response.StopReason == \u0026#34;max_tokens\u0026#34;) { pars.Messages.Add(new Message(RoleType.Assistant, response.Message)); pars.Messages.Add(new Message(RoleType.User, \u0026#34;bitte fahre fort\u0026#34;)); response = await client.Messages.GetClaudeMessageAsync(pars); translated += response.Message.ToString(); } if (response.StopReason != \u0026#34;end_turn\u0026#34;) throw new Exception($\u0026#34;Translation failed. Stop reason:{response.StopReason}\u0026#34;); return translated; } ","date":"2025-01-16T12:00:00+01:00","permalink":"/blog/automatic-blogpost-translation-with-claude-ai/","title":"Automatic Blogpost translation with Claude.ai"},{"content":"I went deep into Home Assistant automation currently and - as everybody knows who ever went this road - more ideas came up. So my newest idea was to use the Hex-Panels from Nanoleaf to display some information. For example if the washing machine is ready, I have an appointment, the Smartphone Battery gets low and so on.\nThis entry describes how I managed to do that.\nI collected all this information from different sources. THe most important was from sygys who posted the Home Assistant configuration: https://community.home-assistant.io/t/nanoleaf-per-tile-light-control-build-in-ha/442720 By the API documentation alone I would not have been able to achieve that.\nThe hardware I mostly used Nanoleaf Elements in the wooden design. These cannot display any colors but just lighten up. I like the descent appearance of those. Do not want to create an all-rgb-gaming-experience in my living room. I also added three dark black hexagon tiles to it. Those where meant to display the status if needed. BTW: The nanoleaf app does not offer any color scenes in this setup because the wooden panels are the \u0026ldquo;main\u0026rdquo; panels. But that does not matter for this scenario.\nIntegrating in Home Assistant For the main features I used the standard integration for Home Assistant. That allows detecing swipe controls (another advantage of the wooden panels because you don\u0026rsquo;t see fingerprints there) and start mostly anything what the app allows. Which does NOT include adressing single panels\nUsing the API Currently this can only be done by a REST-Api. Initially you need any tool that can send POST - Commands. You can use Curl if you like to work on the shell, or UI-Tools like Postman.\nRetrieving a secret The first manual step is to obtain a secret. This is done exactly like when installing the Home Assistant integration. You just pair the Panels by pressing the power button for a few seconds until the LEDs start blnking/moving. Don\u0026rsquo;t worry, this will NOT remove any previous pairings. Now while the pairing is active call the following URL:\nPOST http://192.168.178.65:16021/api/v1/new\n(replace the IP by the Nanoleaf IP. Should be visible in your Router for example)\nThis will return just a simple value which contains your secret token. This token will remain valid even if you unpower the Nanoleaf. Make sure you store this secret somewhere, you will need it.\nRetrieve the Configuration Now it is time to test your secret and retrieve the configuration from your Nanoleaf panels. You even can do this in your browser, or still in Postman/Curl:\nhttp://192.168.178.65:16021/api/v1/thisisthemagicsecretreturnedbefore/\nThe most interesting part is the layout/positionData-Part in that response:\n\u0026#34;layout\u0026#34;: { \u0026#34;numPanels\u0026#34;: 46, \u0026#34;sideLength\u0026#34;: 0, \u0026#34;positionData\u0026#34;: [ { \u0026#34;panelId\u0026#34;: 8172, \u0026#34;x\u0026#34;: 301, \u0026#34;y\u0026#34;: 747, \u0026#34;o\u0026#34;: 360, \u0026#34;shapeType\u0026#34;: 15 }, { \u0026#34;panelId\u0026#34;: 36653, \u0026#34;x\u0026#34;: 276, \u0026#34;y\u0026#34;: 703, \u0026#34;o\u0026#34;: 300, \u0026#34;shapeType\u0026#34;: 15 }, [..] I honestly was a bit surprised, because I do NOT have 46 panels. Far from it. The simple truth is: THe wooden Panels have 6 LEDs. each of them has their own ID. So having 7 wooden panels that makes 42 Ids, additional 3 Ids for the three black panels and the control buttons is an additional panel (with id 0)\nIf you do not need to adress all Panels but just a single one, a good hint is to just rotate one of the panels, make that call again and see which panel has a new orientation (\u0026ldquo;o\u0026rdquo; in the json)\nNow you have to write down all PanelIds. At least those you want to modifiy. Because I mostly want to use the black panels I split the following part in two, but you can also work with one (or even more).\nConfigure Rest in Home Assistant Go to your beloved Home Assistant Configuration (configuration.yaml) and add the following entries: {% raw %}\nrest_command: nanoleaf_wood: url: http://192.168.178.65:16021/api/v1/thisisthemagicsecretreturnedbefore/effects method: PUT payload: \u0026gt; { \u0026#34;write\u0026#34; : {\u0026#34;command\u0026#34;: \u0026#34;display\u0026#34;, \u0026#34;animType\u0026#34;: \u0026#34;static\u0026#34;, \u0026#34;animData\u0026#34;: {%- set all = [ 8172,36653, 32365,61100,6889,35368,57498,28763,33051,4570,9112,45913,61802,25003,37099,42,14434,43171,34741,29680,58161,4721,33456,45298,4960,8482,45539,16547,53346,9255,5361,58801,30064,33077,4596,57524,54769,50577,21840,42000,13521,1683] %} {%- set ns = namespace(panels=[panels | count | string]) %} {%- for panel in panels %} {%- set ns.panels = ns.panels + [ \u0026#39;{} 1 {} {} {} 0 20\u0026#39;.format(all[panel.number-1], panel.r, panel.g, panel.b) ] %} {%- endfor %} \u0026#34;{{ ns.panels | join(\u0026#39; \u0026#39;) }}\u0026#34;, \u0026#34;loop\u0026#34;: false, \u0026#34;palette\u0026#34;: [], \u0026#34;colorType\u0026#34;: \u0026#34;HSB\u0026#34;} } content_type: \u0026#34;application/json\u0026#34; nanoleaf_black: url: http://192.168.178.65:16021/api/v1/thisisthemagicsecretreturnedbefore/effects method: PUT payload: \u0026gt; { \u0026#34;write\u0026#34; : {\u0026#34;command\u0026#34;: \u0026#34;display\u0026#34;, \u0026#34;animType\u0026#34;: \u0026#34;static\u0026#34;, \u0026#34;animData\u0026#34;: {%- set all = [ 52732, 44680, 17782 ] %} {%- set ns = namespace(panels=[panels | count | string]) %} {%- for panel in panels %} {%- set ns.panels = ns.panels + [ \u0026#39;{} 1 {} {} {} 0 20\u0026#39;.format(all[panel.number-1], panel.r, panel.g, panel.b) ] %} {%- endfor %} \u0026#34;{{ ns.panels | join(\u0026#39; \u0026#39;) }}\u0026#34;, \u0026#34;loop\u0026#34;: false, \u0026#34;palette\u0026#34;: [], \u0026#34;colorType\u0026#34;: \u0026#34;HSB\u0026#34;} } content_type: \u0026#34;application/json\u0026#34; {% endraw %} Replace the IP with your Nenoleaf - IP and the secret with your secret (before \u0026ldquo;/effects\u0026rdquo;). It is up to you if you want one or more different calls. The numbers in the \u0026ldquo;set all\u0026rdquo; - part have to be replaced by the panelIds you retrieved earlier.\nWhat comes next is of course testing your config and then restarting Home Assistant.\nAutomation Examples Now you can use this in any automation. The number is the number in the array, starting at 1(!) Example for the simple ones:\nalias: Nano_BlackTest description: \u0026#34;\u0026#34; trigger: [] condition: [] action: - service: rest_command.nanoleaf_black data: panels: - number: 1 r: 255 g: 0 b: 0 - number: 2 r: 0 g: 255 b: 0 - number: 3 r: 0 g: 0 b: 255 mode: single `and an example to light up the three wooden panels (remember. Each panel has 6 LEDs):` alias: Nano_WoodTest description: \u0026#34;\u0026#34; trigger: [] condition: [] action: - service: rest_command.nanoleaf_wood data: panels: - number: 31 r: 255 g: 255 b: 255 - number: 32 r: 255 g: 255 b: 255 - number: 33 r: 255 g: 255 b: 255 - number: 34 r: 255 g: 255 b: 255 - number: 35 r: 255 g: 255 b: 255 - number: 36 r: 255 g: 255 b: 255 - number: 19 r: 255 g: 255 b: 255 - number: 20 r: 255 g: 255 b: 255 - number: 21 r: 255 g: 255 b: 255 - number: 22 r: 255 g: 255 b: 255 - number: 23 r: 255 g: 255 b: 255 - number: 24 r: 255 g: 255 b: 255 - number: 1 r: 255 g: 255 b: 255 - number: 2 r: 255 g: 255 b: 255 - number: 3 r: 255 g: 255 b: 255 - number: 4 r: 255 g: 255 b: 255 - number: 5 r: 255 g: 255 b: 255 - number: 6 r: 255 g: 255 b: 255 mode: single That\u0026rsquo;s it. Have fun with your panels-\n","date":"2024-01-20T12:00:00+01:00","permalink":"/blog/nanoleaf/","title":"Setting single Panels on NanoLeaf using Home Assistant"},{"content":"[Disclaimer: This is not meant as a tutorial, more as some chitchat. You can find the source on github nonetheless, but I assume this is a VERY specific use case noone else uses]\nLet\u0026rsquo;s start by telling you that this whole project ist utter nonsense. Noone really needs this feature. I decided to create this nonetheless.\nThe Goal I own a little feeding box for squirrels of the neighbourhood. And it works as designed: Squirrels get here, steal their food and run away.\nI can see them through my glass door and it just looks amazing, especially in the typical \u0026ldquo;Hamburger Wetter\u0026rdquo; (Hamburg weather, means: raining horizontally). Sometimes I take a picture or even a video, but through the door that just does not look very great.\nSo I decided to add a surveillance cam to the mix, and while I am on it: Automatically publish whenever a squirrel appears\nHardware I use a dlink - outdoor webcam with WiFi. Not really worth looking up the model number here, just the cheapest WiFi-Cam I could find which is compatible with Synology Surveillance Station (but nearly all are)\nIn addition the Synology NAS with installed surveillance station. I own a DS920+, but any should do\nPrivacy I do not want to track any persons. Not me, not others. This is not meant to be surveillance, it is meant to film squirrels. TBH I did not ask the squirrels how they think about privacy and GDPR, but I assume they are fine with it. After all they accepted all Cookies until now, and not only the required ones.\nTo make sure only squirrels (or other animals) get caught I wanted to do a few conditions:\nOnly track movement directly in front of the feeder. So I can make sure not Humans or Traffic in the background trigger the motion sensor Put the Camera in \u0026ldquo;Squirrel\u0026rdquo;-height. So even if someone (me or my guests) manages to trigger the alarm no faces can be seen Have a simple to use approval workflow Just in case. If someone ends up lying there drunk I do not want to publish a video of that person anywhere Only Track movement directly in front of the feeder That is one of the reasons why I use the Surveillance Station of Synology. The Camera itself has movement detection and (according to the documentation) even a fancy AI \u0026ldquo;this is a person\u0026rdquo; algortihm. I do not want the last for obvious reasons and especially can not decide which area of the image should be used for detection. In addition this seems to require a cloud feature. I do not want my camera feed in the cloud from anyone. This should never leave my house without my consent. The surveillance station can select very small pieces of the image to be selected as alert regions. I put it directly in front of the feeder where no background movement can be detected. This way I can also adjust the sensibility of the detection to quite a high value because no noise becaues of branches moving in the wind etc. is to be expected.\nPut the camera in squirrel-height That was easy as there are some flowerboxes directly before the feeder where I mounted the cam Have a simple approval workflow Because I mostly am at Mastodon at time of this writing (these sentences sometimes age very bad. A few months ago that would have been the artist formerly known as Twitter, which itself was just a replacement for Google+ which, well, you know the sad story). Let\u0026rsquo;s just hope that was the last Social Media switch for me.\nOne of the advantages of Mastodon is that it has a very nice, simple API that allows sending toots, getting Notifications and so on. So my idea for the workflow is to notify me (and only me) that a new movement has been detected. Attach an image of that movement and if I approve this (e.g. by adding a \u0026ldquo;fav\u0026rdquo; to that toot) the bot knows that this detection was a valid Squirrel video and posts it. So AI without the \u0026ldquo;A\u0026rdquo;. (some might argue even without the \u0026ldquo;I\u0026rdquo;\u0026hellip;)\nSynology Adding the Webcam is quite simple. It nearly is found automatically in Surveillance station. Finding out how to connect was a bit harder. Username is always \u0026ldquo;admin\u0026rdquo; and the required password is a PIN. Printed onto the Power Plug, not the device\u0026hellip; After that the App of the camera does not like the Camera anymore, which is bonus for me as I do not want images to be uploaded to dlink, anyways.\nThe detection area can be set in the Camera options. Fiddling with the thresholds takes a while, especially as I do not have a trained squirrel to assist me on that. I then changed the time plan to always motion detection (so disabling the constant recording) and updated the minimum record time to five minutes. A good comprimise between getting everything and not creating videos that are too big for mastodon/youtube/wherever I wanted to upload this, which I had not decided at this point\nSynology has multiple ways to notify about movements: Email, SMS, Push notifications and Webhooks. Obviously the last one was the way to go as it does not spam me in any way and I have to program stuff anyways.\nWriting a Webhook A webhook is just an HTTP-call essentially. Synology allows POST and PUT in Surveillance Station. So I created a simple Web Api in Visual Studio and was ready to debug the incoming data.\nFirewalls, Firewalls everywhere! Synology allows you to send a test-notification which just resulted in an error. WHAT exactly went wrong is nowhere to be seen. Not in surveillance station and not in the logs from the synology. So I had to fly blind at this point.\nI knew it worked when using my local machine, either via localhost or the ipadress from my development machine.\nSo the next step was to connect to my synology via SSH and try to access my API throuh a simple webcall via GET: wget https://192.168.178.23:6789/ping\nThis simple call already did not work. So must have been a firewall issue. I checked everything in my Synology configuration FOR A VERY LONG TIME and did find nothing. Finally I put my attention to Windows, disabled the Defender Firewall and it went through. Because I do not want to have the firewall open I added an exception through the \u0026ldquo;allow an app through windows firewall\u0026rdquo; and finally it did work with the firewall still enabled. Receiving data from the webhook The Webhook arrived as expected, has some informations in it and can even provide an url to a preview image. That image URL is a generated \u0026ldquo;public\u0026rdquo; (within my LAN) Address I am able to receive and download:\napp.MapPost(\u0026#34;/squirrel\u0026#34;, async context =\u0026gt; { var requestBody = await context.Request.ReadFromJsonAsync\u0026lt;SynologyBody\u0026gt;(); if (requestBody != null) { if (!string.IsNullOrWhiteSpace(requestBody.Capture)) { var previewImage = await downloader.DownloadFile(requestBody.Capture); } else { Console.WriteLine(\u0026#34;received event without image. Will ignore it\u0026#34;); } } await context.Response.WriteAsync(\u0026#34;event received\u0026#34;); }); public class SynologyBody { public string Subject { get; set; } public string Desc { get; set; } public string Capture { get; set; } } (Synology does not really care about the response but I found it useful when testing locally through Postman)\nAt this point I do not have a video yet which is to be expected, as \u0026ldquo;normal\u0026rdquo; users would expect to receive the warning immediately and not five minutes after someone stole their nuts.\nSend private notification to Mastodon After an event have been received the Image is downloaded and a message is sent to Mastodon using MastoNet:\nprivate MastodonClient Login() { return new MastodonClient(_instance, _accessToken); } public async Task\u0026lt;Status?\u0026gt; SendPreview(Stream previewFile, DateTime notificationDate) { var client = Login(); var text = $\u0026#34;{_approver} ALARM! Possible Squirrel detection! ALARM! #preview DATE:\\n{notificationDate}\\n\u0026#34;; var media=await client.UploadMedia(previewFile, \u0026#34;squirrel.png\u0026#34;, \u0026#34;possibly a squirrel\u0026#34;); var status=await client.PublishStatus(text, Visibility.Direct, mediaIds: new[] {media.Id }); if (string.IsNullOrWhiteSpace(status.Id)) { Console.WriteLine(\u0026#34;Could not send preview\u0026#34;); return null; } Console.WriteLine($\u0026#34;Published to Mastodon with id {status.Id}\u0026#34;); return status; } This creates the following toot: Waiting for approval This toot has only been sent to me privately (mastodon has no encryption so always keep that in mind about the \u0026ldquo;privacy\u0026rdquo; - part). I can then decide that this is a valid Squirrel detection by setting the \u0026ldquo;fav\u0026rdquo; star of that toot.\nKeeping the state To be able to understand which previewtoot belongs to what webhook call I need to store the state. I uses LitDB for this. LiteDB is a very simple document database which needs no installation at all. It completely only lives within the NuGet-Package and the data is stored in a Bson-Format (Json, but compressed). It is a bit like simply serializing my contents and store it into a Json file. Just performing much better.\nThat is why the complete \u0026ldquo;database\u0026rdquo;-class is as simple as this:\npublic class DataBase { private const string _databaseName = @\u0026#34;Filename=squirrel.db;connection=shared\u0026#34;; public T GetContents\u0026lt;T\u0026gt;(string id) { using var db = new LiteDatabase(_databaseName); return db.GetCollection\u0026lt;T\u0026gt;().FindById(new BsonValue(id)); } public void RemoveContents\u0026lt;T\u0026gt;(string id) { using var db = new LiteDatabase(_databaseName); db.GetCollection\u0026lt;T\u0026gt;().Delete(new BsonValue(id)); } public void UpsertContents\u0026lt;T\u0026gt;(T contents) { using var db = new LiteDatabase(_databaseName); db.GetCollection\u0026lt;T\u0026gt;().Upsert(contents); } public SquirrelData? FindByFilename(string filename) { using var db = new LiteDatabase(_databaseName); return db.GetCollection\u0026lt;SquirrelData\u0026gt;().FindOne(q =\u0026gt; q.ExpectedMovieFile == filename); } } Now the data inside the Database contains the following stuff:\npublic class SquirrelData { [BsonId] public string Id { get; set; } // First MastodonID public string? PublicId { get; set; } public DateTime EventReceived { get; set; } public DateTime SentForApproval { get; set; } public DateTime? SentPublic { get; set; } public DateTime? Faved { get; set; } public string? ExpectedMovieFile { get; set; } } The Id simply is the Id from the message just posted to mastodon. This object mostly contains Mastodon-Information and Information about the WebHook event.\nAs mentioned above the WebHook has no information about the Video that is created (or most likely: Is still being created when the WebHook is sent). So we simply have to find it. The rules are quite simple: Within the directory with surveillance videos it is simply the newest file we can find.\nOne problem remains: The preview image was within the WebHook and did not need authentication do be downloaded. The surveillance directory does need authentication. So I created a user for that specific purpouse allowed it to access the folder and needed to do some SMB-Stuff for authentication.\nI used the SMBLibrary for that. It isn\u0026rsquo;t as straight forward as navigating through a filesystem, but it does work fine. First I have to connect using my credentials:\npublic void Connect() { var smbClient = new SMB1Client(); bool isConnected = smbClient.Connect(IPAddress.Parse(_server), SMBTransportType.DirectTCPTransport); if (isConnected) { NTStatus status = smbClient.Login(string.Empty, _username, _password); if (status == NTStatus.STATUS_SUCCESS) { _smbClient=smbClient; return; } } throw new Exception(\u0026#34;Connecting failed. Check your credentials\u0026#34;); } After successfully connecting I can access the share and search for the most current mp4-file recursively:\npublic string FindMostCurrentVideo() { var store = _smbClient.TreeConnect(\u0026#34;surveillance\u0026#34;, out var status); if (status != NTStatus.STATUS_SUCCESS) throw new Exception(\u0026#34;Connecting to Share failed\u0026#34;); var mostCurrentFile=GetMostCurrentFileRec((SMB1FileStore)store, _startPath); return Path.Combine(mostCurrentFile.Path, mostCurrentFile.Filename); } private MostCurrentFile GetMostCurrentFileRec(SMB1FileStore store, string folder) { folder += \u0026#34;\\\\\u0026#34;; var mostCurrentFile = new MostCurrentFile { Created = DateTime.MinValue }; var status=store.QueryDirectory(out var subdirectoryList, folder, SMBLibrary.SMB1.FindInformationLevel.SMB_FIND_FILE_DIRECTORY_INFO); if (status== NTStatus.STATUS_SUCCESS) { object directoryHandle; FileStatus fileStatus; status = store.CreateFile(out directoryHandle, out fileStatus, folder, AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory | SMBLibrary.FileAttributes.Normal, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); ; if (status == NTStatus.STATUS_SUCCESS) { List\u0026lt;FindInformation\u0026gt; fileList2; status = store.QueryDirectory(out fileList2, folder+ \u0026#34;*\u0026#34;, FindInformationLevel.SMB_FIND_FILE_DIRECTORY_INFO); var dirInfo = fileList2.Where(q =\u0026gt; q.GetType() == typeof(FindFileDirectoryInfo)).Cast\u0026lt;FindFileDirectoryInfo\u0026gt;(); var subdirectories=dirInfo.Where(q=\u0026gt;q.ExtFileAttributes.HasFlag(ExtendedFileAttributes.Directory)); var files=dirInfo.Where(q =\u0026gt; !q.ExtFileAttributes.HasFlag(ExtendedFileAttributes.Directory)); // Look for subdirectories and drill down to them: foreach (var directory in subdirectories) { if (directory.FileName.Contains(\u0026#34;.\u0026#34;)) continue; var mostCurrentSubFile = GetMostCurrentFileRec(store, Path.Combine(folder, directory.FileName)); if (mostCurrentSubFile.Created \u0026gt; mostCurrentFile.Created) mostCurrentFile = mostCurrentSubFile; } // Look for .mp4 - Files in current directory and check if they are newer than the most current file known: foreach (var file in files.Where(q=\u0026gt;q.FileName.EndsWith(\u0026#34;.mp4\u0026#34;))) { if (file.CreationTime \u0026gt; mostCurrentFile.Created) { mostCurrentFile = new MostCurrentFile { Created = file.CreationTime.Value, Filename = file.FileName, Path = folder }; } } status = store.CloseFile(directoryHandle); } } return mostCurrentFile; } At this point I do not need the filecontents itself, but only the path so I can use it lateron.\nAfter that we have the whole WebHook complete:\nReveive the Contents (including preview image) from the webhook Determine which Video File matches CHeck if we already had this file (if within a video multiple webhooks are fired) and avoid duplicates Send a notification to Mastodon Store all known information into our database. So our Webhook from the beginning now looks like this:\napp.MapPost(\u0026#34;/squirrel\u0026#34;, async context =\u0026gt; { var received = DateTime.Now; Console.WriteLine(\u0026#34;Received squirrel Event\u0026#34;); var requestBody = await context.Request.ReadFromJsonAsync\u0026lt;SynologyBody\u0026gt;(); if (requestBody != null) { if (!string.IsNullOrWhiteSpace(requestBody.Capture)) { var previewImage = await downloader.DownloadFile(requestBody.Capture); downloader.Connect(); string videoFilename = downloader.FindMostCurrentVideo() ?? throw new Exception(\u0026#34;No Video Found\u0026#34;); var existingItem = database.FindByFilename(videoFilename); if (existingItem != null) { Console.WriteLine($\u0026#34;\u0026#39;{videoFilename}\u0026#39; already processed with MastodonId {existingItem.Id}\u0026#34;); await context.Response.WriteAsync(\u0026#34;duplicate event\u0026#34;); return; } var mastodonStatus = await mastodon.SendPreview(previewImage, received); if (mastodonStatus != null) { database.UpsertContents(new SquirrelData { EventReceived = received, ExpectedMovieFile = videoFilename, Id = mastodonStatus.Id, SentForApproval = mastodonStatus.CreatedAt }); } } } await context.Response.WriteAsync(\u0026#34;event received\u0026#34;); }); Checking for Notifications As mentioned the \u0026ldquo;approval workflow\u0026rdquo; (a big word for this simple mechanism, I know) is to wait for a fav. If the fav is received it means I decided this is a valid Squirrel detection and the video can be published.\nAlso do not wait longer than 7 days. If I did not approve in that time, it probably is not a good video.\nAgain using MastoNet I can receive the Notifications for that user:\npublic async Task\u0026lt;List\u0026lt;Notification\u0026gt;\u0026gt; GetFavs() { var favourites= new List\u0026lt;Notification\u0026gt;(); var client=Login(); var notifications=await client.GetNotifications(); foreach (var notification in notifications) { switch (notification.Type) { case \u0026#34;mention\u0026#34;: await client.PublishStatus($\u0026#34;Sorry. I am just a bot and will not respond to this. Ask {_approver} if you want to know anything about what I do\u0026#34;, replyStatusId: notification.Id); await client.DismissNotification(notification.Id); break; case \u0026#34;favourite\u0026#34;: favourites.Add(notification); break; default: Console.WriteLine($\u0026#34;discarding notification of type {notification.Type}\u0026#34;); await client.DismissNotification(notification.Id); break; } } return favourites; } The nofication.Status.Id now is the id we already stored in the Database and can use to find all known WebHook-Data of that event and send a new Toot. This time just with the Video file instead of the preview image.\nWhy no Mastodon Webhooks? The Mastodon API also offers Webhooks. So instead of periodially polling for new notifications this could be used to receive the favs. The main reason why I did not use this (apart from the fact that polling is simply fast enough) is the fact that my application is not accessible from the outside. To allow this I would need to make my NAS (where the application is hosted) accessible from the Internet. And I do not want that. I could add Firewall rules to only allow specific ports and so on, but no access is even a bit better than a limited access. Even if noone would get through, just bots trying the flood my machine with requests is a headache I do not need.\nUploading the Video After trying to uplaad the Video directly to Mastodon the next problem came up: The file was too big. While it was way inside the limits the upload (to the mastodon.social instance) just responded with an error \u0026ldquo;something went wrong\u0026rdquo;. I suspect the Library to be the main issue as the Upload is not done async and so a timeout might be the cause.\nThat is why I decided to upload the Video to YouTube instead. YouTube provides APIs to do that. The main issue is that this also requires an OAuth authentication. Luckily the documentation for this are quite straight-forward so a simple Test worked fine:\npublic async Task UploadVideo(Stream fileStream) { var youtubeService = new YouTubeService(new BaseClientService.Initializer() { ApiKey = _apiKey, ApplicationName = this.GetType().ToString() }); UserCredential credential; using (var stream = new FileStream(\u0026#34;secrets.json\u0026#34;, FileMode.Open, FileAccess.Read)) { credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.Load(stream).Secrets,new[] { YouTubeService.Scope.YoutubeUpload },\u0026#34;user\u0026#34;,CancellationToken.None); } youtubeService = new YouTubeService(new BaseClientService.Initializer() { HttpClientInitializer = credential, ApplicationName = Assembly.GetExecutingAssembly().GetName().Name }); var video = new Video(); video.Snippet = new VideoSnippet(); video.Snippet.Title = \u0026#34;Squirrel detected\u0026#34;; video.Snippet.Description = \u0026#34;You should have bought a squirrel\u0026#34;; video.Snippet.Tags = new string[] { \u0026#34;squirrel\u0026#34; }; video.Snippet.CategoryId = \u0026#34;22\u0026#34;; video.Status = new VideoStatus { MadeForKids = false, SelfDeclaredMadeForKids = false }; video.Status.PrivacyStatus = \u0026#34;private\u0026#34;; var videosInsertRequest = youtubeService.Videos.Insert(video, \u0026#34;snippet,status\u0026#34;, fileStream, \u0026#34;video/*\u0026#34;); videosInsertRequest.ProgressChanged += videosInsertRequest_ProgressChanged; videosInsertRequest.ResponseReceived += videosInsertRequest_ResponseReceived; await videosInsertRequest.UploadAsync(); } private void videosInsertRequest_ResponseReceived(Video obj) { Console.WriteLine($\u0026#34;done\u0026#34;); } private void videosInsertRequest_ProgressChanged(IUploadProgress obj) { Console.WriteLine($\u0026#34;Bytes: {obj.BytesSent}| Status: {obj.Status}\u0026#34;); } (secrets.json contains a client_id and client_secret for oauth) While uploading the ProgressChanged event gets fired a few times. Obj.Status cycles from Starting, Uploading to Completed. Which is also the Status when ResponseReceived is fired.\nBecause I had to switch to YouTube I also changed the workflow a bit. Now I already upload the video when posting the preview toot as private on YouTube. So I can verify the complete contents before an approval. And YouTube can process the video while it is waiting for approval. At the end I just switch from private to unlisted if it is approved or delete the Video if no approval has been made for a period of time.\nAnother Pitfall YouTube does not allow using service accounts when uploading Videos. This was crucial for my application because this should run on a server. While the code above works I hit a dead end here. So back to Mastodon:\nFfMpeg To reduce the filesize I went to FFMpeg. I can compress Videos and - if required - even resize them. Because I do not want to call something on the console, I needed a wrapper. I found FfmpegCore which does exactly what I want:\npublic async Task\u0026lt;bool\u0026gt; ResizeVideo(string source, string target) { return await FFMpegArguments .FromFileInput(source) .OutputToFile(target, true, options=\u0026gt;options .WithVideoCodec(VideoCodec.LibX265) .WithVideoFilters(filter=\u0026gt;filter .Scale(videoSize: FFMpegCore.Enums.VideoSize.FullHd) ) .WithFastStart()) .ProcessAsynchronously(); } This tiny little piece of code takes any videofile, compresses it as h265 (using default quality settings) and resized it to FullHD (1080 height) maintaining the correvt aspect ratio.\nThat all works fine on my machine, but because I want to host this (like all my bots) on my Synology-NAS I had to dockerize the contents. I was in luck this time, but this also turned out to be quite easy:\nRUN apt-get -y update \u0026amp;\u0026amp; apt-get -y upgrade \u0026amp;\u0026amp; apt-get install -y --no-install-recommends ffmpeg A single Line in the DockerFile and everything was working smoothly\nFixing the Api The reduced Filesize did not solve the problem that Mastodon did not want to upload my file. So I digged a bit deeper into the Mastodon API. The media upload used by MastoNet seemed to marked as depricated (v1) exactly for the reason why I did not work in my project: It was meant for small preview images, not for Video files.\nA V2-Version is available but not implemented by MastoNet. So I forked the packages from Github and added a V2-Version:\npublic async Task\u0026lt;Attachment\u0026gt; UploadMediaAsync(MediaDefinition media, string? description = null, AttachmentFocusData? focus = null) { media.ParamName = \u0026#34;file\u0026#34;; var list = new List\u0026lt;MediaDefinition\u0026gt;() { media }; var data = new Dictionary\u0026lt;string, string\u0026gt;(); if (description != null) { data.Add(\u0026#34;description\u0026#34;, description); } if (focus != null) { data.Add(\u0026#34;focus\u0026#34;, $\u0026#34;{focus.X},{focus.Y}\u0026#34;); } return await this.Post\u0026lt;Attachment\u0026gt;(\u0026#34;/api/v2/media\u0026#34;, data, list); } This was confusingly easy as I simpy just replaced \u0026ldquo;v1\u0026rdquo; by \u0026ldquo;v2\u0026rdquo;. Still it worked. Problem was: The next step, usually taking the returned Id and add this to a toot failed, because Mastodon was not finished, yet. I could simply have waited a long enough time, but decided to add a poll to receive status updates. Every Media of a known I can be requested. There is no statusflag about it processing but some properties give a hint: First there is the previewUrl this is filled at the very start after uploading. Tbe Url-Property, however isn\u0026rsquo;t. This will always be Null until everything is processed. So just requesting the Media every 10 seconds until the Url is filled did the trick;\npublic async Task\u0026lt;Attachment\u0026gt; WaitUntilMediaIsUploaded(Attachment attachment, int waitSeconds = 10, int maxWaitSeconds = 300) { int totalTimeWasted = 0; while (attachment.Id != null \u0026amp;\u0026amp; attachment.Url == null \u0026amp;\u0026amp; attachment.PreviewUrl != null \u0026amp;\u0026amp; totalTimeWasted \u0026lt; maxWaitSeconds) { Thread.Sleep(waitSeconds * 1000); totalTimeWasted += waitSeconds; attachment = await GetMedia(attachment.Id); } return attachment; } public async Task\u0026lt;Attachment\u0026gt; GetMedia(string mediaId) { var attachment = await this.Get\u0026lt;Attachment\u0026gt;(\u0026#34;/api/v1/media/\u0026#34; + mediaId); var prev = attachment.PreviewUrl; var url = attachment.Url; return attachment; } Finally So that was the last Pitfall. Now everything works:\nIf a motion is detected my WebHook gets called The Webhook posts a previewimage as a direct toot (only be seen by me) to Mastodon including a Preview Image If \u0026ldquo;fav\u0026rdquo; the toot when this is a valid Squirrel detection The Bot checks for new Favs and finds mine The according Video is found in the LiteDB The Video gets downloaded via SMB-Stuff The Video gets Resized by FfMpeg The resized video gets uploaded to Mastodon A public toot containing the video is created (Using standard Mastodon-Features, all toots will get deleted after two weeks reducing the space required)\nBut WHY? I am very aware that this was mostly a waste of time. A lot of time for a simple \u0026ldquo;Lets show when there is a squirrel\u0026rdquo;. Simone Giertz once did a great TED - Talk Why you should make useless things. While she talks about robots, by fun thing to do was alway coding. No matter how useless the results are. So: Yes. I totally agree with her. Creating stupid things just because it is fun, with noone asking why this takes so long and even be able to quit if something becomes boring is a nice diversion to the normal coding reality I have for paying the rent.\nI hope you also had some fun reading this; I had writing it :)\n","date":"2023-12-17T12:00:00+01:00","image":"/blog/squirrel/feeder_hu_5e0b693f30391cc3.png","permalink":"/blog/squirrel/","title":"How to track a squirrel"},{"content":"At the time of this writing the boot-from-usb has just been made available as a Beta feature for the Raspberry Pi 4.\nThere are a few tutorials on how to activate that feature but I had an additional requirement: Boot from USB first, then boot from SD which requires a few additional steps.\nThis document might still be useful if you just want the SD first approach. Just ignore the second last part in that case.\nBut before we begin:\nUse at your own risk. We are writing data to the EEPROM here using beta contents and even manipulate configs for that. If something fails your pi might be bricked. Not even reinstall Linux will repair that.\nAdditional warning: Don\u0026rsquo;t try this on a N00bs install.\nStill here? So let\u0026rsquo;s begin. (I will asume you use Raspbian Buster)\nUpdating the Pi Before we start we should always have the most current Linux version installed. So make sure your internet connection works and enter\nsudo apt-get update\nfollowed by\nsudo apt-get upgrade\nand in addition\nsudo rpi-update\nto update the firmware on the sdcard\nSwitching to beta now change rpi to switch to beta - state. (If you are reading this from the future: Hi there! Cars still a thing? And maybe you don\u0026rsquo;t need to switch to beta anymore)\ndo this by using your favourite editor like vi or nano to change the rpi-config:\nsudo nano /etc/default/rpi-eeprom-update\nchange the release-status from critical to beta and save the file.\nFlashing the EEPROM Now check the most current version on your pi:\nls /lib/firmware/raspberrypi/bootloader/beta/pieeprom*.bin\nAt time of this writing the most current version there is 2020-05-15. So flash the eeprom with that version:\nsudo rpi-eeprom-update -d -f /lib/firmware/raspberrypi/bootloader/beta/pieeprom-2020-05-15.bin\nYou will be thrown back to the prompt a while after. Now reboot your pi and hope nothing went wrong:\nsudo shutdown - r now\nnow check if the version has been installed by typing\nsudo vcgencmd bootloader_version\nthe response should display the date corresponding to your file. So something like May 15\nChanging the boot order By default now the boot order will be SD first, USB second. So if you just want to throw away your SD and boot grom USB: Ignore this chapter. If you need a \u0026ldquo;hardware multiboot\u0026rdquo; (boot from USB if attached but still have a fallback SD in place if not) follow these instructions:\nFirst make sure I did not lie about the default order and display the current config:\nsudo vcgencmd bootloader_config\nthis should display some information about the configuration. Most interesting for us is the last part called BOOT_ORDERwhich will contain the entry\nBOOT_ORDER=0xf41\nwhere 4=USB and 1=SD. More details like network boot can be found at the bootloader documentation.\nWhile this might look like just how we want it you have to read this right to left. So it is SD (1) first and USB (4) second. But we are about to change that.\nFirst it is highly recommend to not fiddle with the official ROM files but make a copy of that. Let\u0026rsquo;s do that:\nsudo cp /lib/firmware/respberrypi/bootloader/beta/pieeprom-2015-05-15.bin pieeprom.bin\nNow create a config from that rom:\nsudo rpi-eeprom-config pieeprom-usbfirst.bin \u0026gt;bootconf.txt\nYou now created a config file containing the config-output which you can edit by nano again:\nsudo nano bootconf.txt\nand change the last line to:\nBOOT_ORDER=0xf14\nAt the last step we now apply our changed config und update the eeprom again:\nsudo rpi-eeprom-config --out pieeprom-usbfirst.bin --config bootconf.txt pieeprom.bin sudo rpi-eeprom-update -d -f ./pieeprom-usbfirst.bin sudo shutdown -r now Finalizing the USB drive Now our raspberry can boot from USB (and boot from there first) but the linux on our USB does not really know how to handle that. This part is simple:\nFirst you need to flash a Raspbian image onto your USB-drive (just like you would to with an SD-card) . Then attach and mount the usb drive and copy the most current firmware files from the /boot - directory of the sdcard to the Usb drive. Alternatively you can select those files from the master-branch on github.\nHave fun :)\n","date":"2020-05-27T12:00:00+02:00","permalink":"/blog/raspberry-pi-4-usb-first/","title":"Raspberry Pi 4: usB first!"},{"content":"Hi,. folks,\nI just programmed a small windows service to automatically FlashAir-pictures when I found out that I lost my FlashAir - card\nSo I am not able to test that program anymore. I decided for myself that I need something better than the unstable FlashAir-Cards and bought a camera with WiFi inside. (Just in case you are curious: It is a Sony HX400)\nI do not publish this as a working download, because I am not able to test it. So there is a big chance that this even breaks the original program. But FAD2 is open source, so I checked the code in into the development-branch on GitHub.\nSo if there is any C# - developer out there who is willing to invest one or two hours to finish that, feel free to do that.\nP.S: No, please don\u0026rsquo;t buy me a new FlashAir :)\n","date":"2017-01-03T12:00:00+01:00","permalink":"/blog/flashairdownloader-development-now-its-your-turn/","title":"FlashAirDownloader - development: Now it's your turn"},{"content":"What the fuzz is this about?\nI am not absolutely sure if I am the first one who did this, but it seems at least noone else wrote it down until now. :) We hat a customer who liked our Office365 Provider-Hosted App \u0026ldquo;Mydea\u0026rdquo;, but wanted us to install that on his SharePoint 2013 - Installation. In Germany a lot of companies do not trust the cloud. So we had three options:\nTell the customer this does not work Copy\u0026amp;paste the whole solution and change it so it works on OnPrem Do something else The first option - of course - wasn\u0026rsquo;t valid for me. The second would have worked. But I did not want to end up in maintanance hell. Our product is about innovation management. And the most innovative about that is the product itself. We develop agile, adding new features (and sometimes bugs) continuously. We do not want to do that twice.\nSo I ended up on the third option: Changing my solution so it will run in an Office365 - SharePoint and on an On-Premise installation of SharePoint 2013 (or above).\nCommon sense about that path seems to be: That does not work. But it does. It just is a real bumby road to get there. This also means: You should know the basics. You should have created at least one Add-In for Office365 and one for SharePoint 2013 or above. I will not explain these steps here (which are complicated enough for local installation). I also cannot help you there in the comments or by mail.\nThere are plenty of documents out there that explain what\u0026rsquo;s needed for this. You should read those, first.\nIf you find this article useful and earn lots of money because of it: Feel free to buy me a beer :)\nAnalyzing the differences: What has to be done? I started with an Office365-App and added the possibility to run that App under an OnPrem-Installation, too. But you should be able to go the other way with little modifications.\nMy first step was to create two dummy \u0026ldquo;Hello World\u0026quot;ish Applications. One for O365 and one for an OnPrem-Installation and look at the source, to \u0026ldquo;diff\u0026rdquo; it. These are the conclusions I came around:\nAt most of us use NuGet-Packages, that was the starting point. There are different Packages: You need the following:\nOnPrem: - AppForSharePointWebToolKit\nOffice 365: - AppForSharePointOnlineWebToolKit - Microsoft.SharePointOnline.CSOM\nThese Packages result in different DLLs with the Prefix \u0026ldquo;Microsoft.SharePoint.Client.*\u0026rdquo;, but different Versions. OnPrem: - Version 15.xxx Office 365: - Version 16.xxx\nThe Sourcecode of the *.cs - files created by that NuGet-Packages (_SharePointContext.cs_ \u0026amp; _Tokenhelper.cs_) are exactly the same. So no need to change anything there.\nFinally you usually store your Office365-Apps on Azure, but don\u0026rsquo;t for On-Premise-Installations. This results in different Authentications: OnPrem: - Using Certificates - Using Windows Authentication\nOffice 365: - Using ClientSecret - Using OAuth Authentication\nThese Informations are stored inside the Web.Config\nBefore we start Create a new Branch in your repository. You never know what can go wrong :)\nThe Web-Project (MVC most likely) Creating different Build configurations The main purpose if this action was to have just one CodeBase for both versions. So new features or bugfixes have only to be made once. No extra code, no branch, no nothing.\nSo I started by creating new Build Configurations. You can create them using \u0026ldquo;Build - Configuration Manager\u0026rdquo;: Create two Configurations. One for Debug and one for Release. Make sure to copy the Settings from the existing \u0026ldquo;Debug\u0026rdquo;- and \u0026ldquo;Release\u0026rdquo;-Configuration At this point, only do this for your web project and - if you have - any backend-projects that are used by your webproject. We will add others later on, but this way you have the chance to validate every single step:\nNow its time to create your config file. Find your web.config, click on it with the right mouse button and select \u0026ldquo;Add config transform\u0026rdquo;. After that you should have a Config file for each configuration\nNuget-References I used the PnP - Packages to develop our O365-Add-In. If you only use the \u0026ldquo;base\u0026rdquo; Packages that come with the Add-In-Solution-Template you might have different packages. Nonetheless you will always have a package \u0026ldquo;AppForSharePointOnlineWebToolKit\u0026rdquo;.\nPackage-wise the main difference between the O365/OnPrem - road are the Azure- and OAuth-packages that are only on the 365-PnP. Also the Office365 Libraries use Version 16, While OnPrem use Version 15.\nTo be able to keep your application updated no matter what configuration you use, create two dummy projects e.g. dummy365 \u0026amp; dummyOnprem. It does not matter what kind of projects these are. The only purpose of these projects is that there are always the packages for both O365 and OnPrem in the ./packages - folder.\nAdd the O365-pnp-NuGet-packages to your dummy365 and the 2013-pnp-nuget-package to your dummyOnPrem\nNow: Before you continue, **make a backup of you .csproj - file. It conains information that will be lost after the next step.\nNow add the \u0026ldquo;other\u0026rdquo; PnP-Package. If you already have the SharePointPnPCoreOnline-Package installed. now install the SharePointPnpCore2013 NuGet-Package (and vice-versa)\ncsProj-Changes You can now diff your csproj - files before and after. Or simply believe my words: There are now two differences. One of them is the OfficeDevPnP.Core - package. The HintPath from the O365 contains SharePointPnPCoreOnline, the HintPath from the 2013-Version contains SharePointPnpCore2013. This does not seem to be an issue as both DLLs are identical. Nonetheless we will have a look at it. Just in case.\nThe more interesting detail is the Reference to \u0026ldquo;Microsoft.Online.SharePoint.Client.Tenant\u0026rdquo;. The Office365 - Version uses Version 16.x and the OnPrem - Version uses 15.x. This is absolutely something we have to care about.\nLuckily, the csProj allows some conditions. So create the following new lines:\n\u0026lt;ItemGroup Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug.OnPrem\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release.OnPrem\u0026#39;\u0026#34;\u0026gt; \u0026lt;Reference Include=\u0026#34;Microsoft.Online.SharePoint.Client.Tenant, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c, processorArchitecture=MSIL\u0026#34;\u0026gt; \u0026lt;HintPath\u0026gt;..\\\\packages\\\\SharePointPnPCore2013.2.9.1611.0\\\\lib\\\\net45\\\\Microsoft.Online.SharePoint.Client.Tenant.dll\u0026lt;/HintPath\u0026gt; \u0026lt;Private\u0026gt;True\u0026lt;/Private\u0026gt; \u0026lt;/Reference\u0026gt; \u0026lt;Reference Include=\u0026#34;OfficeDevPnP.Core, Version=2.9.1611.0, Culture=neutral, PublicKeyToken=3751622786b357c2, processorArchitecture=MSIL\u0026#34;\u0026gt; \u0026lt;HintPath\u0026gt;..\\\\packages\\\\SharePointPnPCore2013.2.9.1611.0\\\\lib\\\\net45\\\\OfficeDevPnP.Core.dll\u0026lt;/HintPath\u0026gt; \u0026lt;Private\u0026gt;True\u0026lt;/Private\u0026gt; \u0026lt;/Reference\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;ItemGroup Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release\u0026#39;\u0026#34;\u0026gt; \u0026lt;Reference Include=\u0026#34;Microsoft.Online.SharePoint.Client.Tenant, Version=16.1.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c, processorArchitecture=MSIL\u0026#34;\u0026gt; \u0026lt;HintPath\u0026gt;..\\\\packages\\\\Microsoft.SharePointOnline.CSOM.16.1.5813.1200\\\\lib\\\\net45\\\\Microsoft.Online.SharePoint.Client.Tenant.dll\u0026lt;/HintPath\u0026gt; \u0026lt;Private\u0026gt;True\u0026lt;/Private\u0026gt; \u0026lt;/Reference\u0026gt; \u0026lt;Reference Include=\u0026#34;OfficeDevPnP.Core, Version=2.9.1611.0, Culture=neutral, PublicKeyToken=3751622786b357c2, processorArchitecture=MSIL\u0026#34;\u0026gt; \u0026lt;HintPath\u0026gt;..\\\\packages\\\\SharePointPnPCoreOnline.2.9.1611.0\\\\lib\\\\net45\\\\OfficeDevPnP.Core.dll\u0026lt;/HintPath\u0026gt; \u0026lt;Private\u0026gt;True\u0026lt;/Private\u0026gt; \u0026lt;/Reference\u0026gt; \u0026lt;/ItemGroup\u0026gt; Do not just copy/paste the content from here! Use the lines from within your csProj-File instead! Versions will change over time. And when you read these lines, my version used might already be outdated.\nAs a last step, remove the original lines from within the main ItemGroup containing these two libraries.\nWell done. You should now try to rebuild your solution on all your four Build configurations, where the OnPrem-Configurations should create a Microsoft.Online.SharePoint.Client.Tenant.dll with Version 15 in your bin-directory and the Office365-Version should create a V16-dll. If you encounter a problem try to unload and reload the projects.\nPlease be advised: I am not sure what happens if you update one of those two Nuget-Packages. You might have to modify your csproj-file again\nWeb.Config Now its time to modify your Web.Configs.\nAdd the following lines just before the closing \u0026quot;\u0026rdquo; section into your *.OnPrem.config - files:\n\u0026lt;runtime\u0026gt; \u0026lt;assemblyBinding xmlns=\u0026#34;urn:schemas-microsoft-com:asm.v1\u0026#34;\u0026gt; \u0026lt;dependentAssembly xdt:Transform=\u0026#34;Replace\u0026#34; xdt:Locator=\u0026#34;Condition(./\\_defaultNamespace:assemblyIdentity/@name=\u0026#39;Microsoft.Online.SharePoint.Client.Tenant\u0026#39;)\u0026#34;\u0026gt; \u0026lt;assemblyIdentity name=\u0026#34;Microsoft.Online.SharePoint.Client.Tenant\u0026#34; publicKeyToken=\u0026#34;71e9bce111e9429c\u0026#34; culture=\u0026#34;neutral\u0026#34; /\u0026gt; \u0026lt;bindingRedirect oldVersion=\u0026#34;0.0.0.0-15.0.0.0\u0026#34; newVersion=\u0026#34;15.0.0.0\u0026#34; /\u0026gt; \u0026lt;/dependentAssembly\u0026gt; \u0026lt;/assemblyBinding\u0026gt; \u0026lt;/runtime\u0026gt; As before: If at time of reading, the most current version is not 15.0, but 15.1 or higher replace the version numbers instead.\nNow it is time to modify your \u0026ldquo;normal\u0026rdquo; Configs for Debug and Release\n\u0026lt;runtime\u0026gt; \u0026lt;assemblyBinding xmlns=\u0026#34;urn:schemas-microsoft-com:asm.v1\u0026#34;\u0026gt; \u0026lt;dependentAssembly xdt:Transform=\u0026#34;Replace\u0026#34; xdt:Locator=\u0026#34;Condition(./\\_defaultNamespace:assemblyIdentity/@name=\u0026#39;Microsoft.Online.SharePoint.Client.Tenant\u0026#39;)\u0026#34;\u0026gt; \u0026lt;assemblyIdentity name=\u0026#34;Microsoft.Online.SharePoint.Client.Tenant\u0026#34; publicKeyToken=\u0026#34;71e9bce111e9429c\u0026#34; culture=\u0026#34;neutral\u0026#34; /\u0026gt; \u0026lt;bindingRedirect oldVersion=\u0026#34;0.0.0.0-16.1.0.0\u0026#34; newVersion=\u0026#34;16.1.0.0\u0026#34; /\u0026gt; \u0026lt;/dependentAssembly\u0026gt; \u0026lt;/assemblyBinding\u0026gt; \u0026lt;/runtime\u0026gt; This is not 100% necessary at this time as these values should already be in the main Web.Config, but this makes sure it still works if that changes. Again: Check the version number.\nAuthentication Your O365 - Config Files should still contain something like this:\n\u0026lt;appSettings\u0026gt; \u0026lt;add key=\u0026#34;ClientValidationEnabled\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;UnobtrusiveJavaScriptEnabled\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;ClientId\u0026#34; value=\u0026#34;aaaaaaaa-beef-face-bbbb-cchcchcchcch\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;ClientSecret\u0026#34; value=\u0026#34;lsdschnabbeldiwuppsfkjashriuae=\u0026#34; /\u0026gt; \u0026lt;/appSettings\u0026gt; `now your OnPrem-configs looks a little bit different:` \u0026lt;appSettings\u0026gt; \u0026lt;add key=\u0026#34;ClientValidationEnabled\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;UnobtrusiveJavaScriptEnabled\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;ClientId\u0026#34; value=\u0026#34;aaaaaaaa-beed-face-bbbb-cchcchcchcch\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;ClientSigningCertificatePath\u0026#34; value=\u0026#34;C:\\\\Certs\\\\myCert.pfx\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;ClientSigningCertificatePassword\u0026#34; value=\u0026#34;p@ssw0rd\u0026#34; /\u0026gt; \u0026lt;add key=\u0026#34;IssuerId\u0026#34; value=\u0026#34;bbbbbbbb-beef-face-bbbb-cchcchcchcch\u0026#34; /\u0026gt; \u0026lt;/appSettings\u0026gt; The main difference here is that you don\u0026rsquo;t just use a ClientId/ClientSecret - Combination for an OnPrem-Installation but have to use a Certificate. I will not describe here how to obtain it. It is just the \u0026ldquo;normal\u0026rdquo; OnPrem-procedure for provider hosted apps. Also be aware that the IssuerId is not the ClientId.\nIf you need help on this part: There are plenty of documents about this. For example onMSDN\nBackend, Webjobs and other projects that talk directly to SharePoint App.Config If you have any other Projects than the Web Project and the AddIn, you might have to do some extra steps. For example if you use a backend-project, you have to modify that, too. Ignore this chapter if you don\u0026rsquo;t have any additional projects.\nBut if you have any console application (like Azure WebJobs) you have to modify those configs, too. Sadly. Config-transform does not work out of the box with App.Config - files. But there is a way to change that.\nFirst: Create your Config Files manually. So select Add - New Item and select \u0026ldquo;Application Config File\u0026rdquo;. Name it App.OnPrem.config. Now open the csProj-File with Notepad. Look for an ItemGroup containing the App.Config. There already should be an App.Debug.config and App.Release.config with a \u0026ldquo;DependentUpon\u0026rdquo; - Attribute. Just copy that attribute to your newly created files. So that you end with something like this:\n\u0026lt;ItemGroup\u0026gt; \u0026lt;None Include=\u0026#34;App.config\u0026#34; /\u0026gt; \u0026lt;None Include=\u0026#34;App.Debug.config\u0026#34;\u0026gt; \u0026lt;DependentUpon\u0026gt;App.config\u0026lt;/DependentUpon\u0026gt; \u0026lt;/None\u0026gt; \u0026lt;None Include=\u0026#34;App.Debug.OnPrem.config\u0026#34; \u0026gt; \u0026lt;DependentUpon\u0026gt;App.config\u0026lt;/DependentUpon\u0026gt; \u0026lt;/None\u0026gt; \u0026lt;None Include=\u0026#34;App.Release.OnPrem.config\u0026#34; \u0026gt; \u0026lt;DependentUpon\u0026gt;App.config\u0026lt;/DependentUpon\u0026gt; \u0026lt;/None\u0026gt; \u0026lt;None Include=\u0026#34;App.Release.config\u0026#34;\u0026gt; \u0026lt;DependentUpon\u0026gt;App.config\u0026lt;/DependentUpon\u0026gt; \u0026lt;/None\u0026gt; \u0026lt;/ItemGroup\u0026gt; After that all your OnPrem-Configs should be displayed as children from App.Config in Visual Studio (2015). Now transform should work. You can check your csproj - File. If there is something like \u0026lt;UsingTask TaskName=\u0026ldquo;TransformXml\u0026rdquo; ../\u0026gt; in it you are ready to go. If not, you are probably using an older Visual Studio version. I would not advise it, but you can go with it, but have to do some extra steps: XDT Transforms for older VS Versions\nNow its the same fun again with the config files as before. So add the same stuff into the App.*.config - files as you have done with the web.configs. In addtion add the transform Property to the configuration tag, so it looks like this: Test what you have done until now Now you should build your Solution for all four configurations. Check if all DLLs where created correctly for the O365 and OnPrem-Configurations. Deploy your application still with the O365 - Settings. Everything should look like before.\nChanges on the Web Project Now it is time to modify your main SharePoint-Project.\nFirst of all: Add your SharePoint-Project to the build configurations. The good thing is: This time you don\u0026rsquo;t have to care about the configs. There will most likely be an app.config in your SharePoint-project, but you can just ignore it, because there are no real references in this project.\nSharePoint - URL The (nearly) final step is to put the right SharePoint-URL to your AddIn-Project depending on your Configuration. The simple way is to just enter the right URL each time you start debugging. The more complex (but better) way is to go the same way as you did with the MVC (Server) part. To do this, enter your Build-Configuration Settings again and set \u0026ldquo;Debug.OnPrem\u0026rdquo; and \u0026ldquo;Release.OnPrem\u0026rdquo; for your AddIn-Project.\nThe URL is stored in the user-settings instead of the project settings. But you can use the same magic to make a conditional path there. Open the file YourApp.csproj.user and add the condition to the \u0026ldquo;SharePointSiteUrl\u0026rdquo; - entry:\n\u0026lt;PropertyGroup\u0026gt; \u0026lt;SharePointSiteUrl Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug.OnPrem\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release.OnPrem\u0026#39;\u0026#34;\u0026gt;http://myDevMachine:10000/sites/Dev\\_Ole/\u0026lt;/SharePointSiteUrl\u0026gt; \u0026lt;SharePointSiteUrl Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release\u0026#39;\u0026#34;\u0026gt;https://my365AppName.sharepoint.com/sites/dev-ole/\u0026lt;/SharePointSiteUrl\u0026gt; \u0026lt;/PropertyGroup\u0026gt; IF you want you can also simply delete the user-file and put these values into your main .csproj. Sadly this cannot be loaded \u0026ldquo;on the fly\u0026rdquo; by switching your Build configuration . You have to unload and reload your project for this setting to be re-read. This is no issue when deploying your solution using an automated build (like tfs online), but while debugging locally.\nTarget office version \u0026amp; SharePoint-URL The csproj - file contains an entry called \u0026ldquo;TargetOfficeVersion\u0026rdquo; which should contain the value \u0026ldquo;16.1\u0026rdquo;. Just like before we need to modify this using conditional values.\n\u0026lt;TargetOfficeVersion Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release\u0026#39;\u0026#34;\u0026gt;16.1\u0026lt;/TargetOfficeVersion\u0026gt; \u0026lt;TargetOfficeVersion Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug.OnPrem\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release.OnPrem\u0026#39;\u0026#34;\u0026gt;15.0\u0026lt;/TargetOfficeVersion\u0026gt; You can validate this setting while selecting the properties of the project and display the \u0026ldquo;Sharepoint\u0026rdquo;-Tab. Under \u0026ldquo;Target Sharepoint version\u0026rdquo; it should say \u0026ldquo;SharePoint 2013\u0026rdquo; if 15.0 is used or \u0026ldquo;SharePoint Online\u0026rdquo; if 16.1 is used.\nAuthentication Office365 uses OAuth for Authentication. For SharePoint 2013 you have to use Windows Authentication instead. As I wrote before: There are a few documents out there explaining the Certificate-Part. But you also have to tell your project to use Windows Authentication. Again we use the same conditions like before, but this time use it for the IISExpressAuth:\n\u0026lt;IISExpressWindowsAuthentication Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug.OnPrem\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release.OnPrem\u0026#39;\u0026#34;\u0026gt;enabled\u0026lt;/IISExpressWindowsAuthentication\u0026gt; \u0026lt;IISExpressWindowsAuthentication Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release\u0026#39;\u0026#34;\u0026gt;disabled\u0026lt;/IISExpressWindowsAuthentication\u0026gt; AppManifest The Appmanifest does not allow any conditional entries. That\u0026rsquo;s why we have to create a copy of it and do the conditional stuff inside the csproj file. So first create a copy called AppManifest.OnPrem.xmlns\nIn that file change the \u0026ldquo;SharePointMinVersion\u0026rdquo; from 16.0.0.0 to 15.0.0.0 in the \u0026ldquo;App\u0026quot;-Section.\nNow it is finally time again to modify the csproj - File. You should be used to it by now :)\n\u0026lt;ItemGroup Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug.OnPrem\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release.OnPrem\u0026#39;\u0026#34;\u0026gt; \u0026lt;AppManifestFile Include=\u0026#34;AppManifest.OnPrem.xml\u0026#34;\u0026gt; \u0026lt;SubType\u0026gt;Designer\u0026lt;/SubType\u0026gt; \u0026lt;/AppManifestFile\u0026gt; \u0026lt;/ItemGroup\u0026gt; \u0026lt;ItemGroup Condition=\u0026#34;\u0026#39;$(Configuration)\u0026#39;==\u0026#39;Debug\u0026#39; Or \u0026#39;$(Configuration)\u0026#39;==\u0026#39;Release\u0026#39;\u0026#34;\u0026gt; \u0026lt;AppManifestFile Include=\u0026#34;AppManifest.xml\u0026#34;\u0026gt; \u0026lt;SubType\u0026gt;Designer\u0026lt;/SubType\u0026gt; \u0026lt;/AppManifestFile\u0026gt; \u0026lt;/ItemGroup\u0026gt; Things to mention on V 15.0 (SharePoint 2013) While most of the code works on Version 15.0 as well on 16.1, there are some minor differences between these two. The following issues appeared on my project: - FieldUserValue has no Property \u0026ldquo;Email\u0026rdquo; - Microsoft.Client.SharePoint.Folder has no \u0026ldquo;Exists()\u0026rdquo; Both work on 16.1 but not on 15.0. So you have to write your own code to get around this issue.\nCustom Compiler Symbols Because of the differences between V16 and V15 you might want to have custom code depending on your configuration. Just like the DEBUG - Symbol you can add your custom Symbol for the OnPrem-Configurations. Just select the properties of each project you need that symbol in, select the Build - tab and into \u0026ldquo;_Conditional compilation symbol_s\u0026rdquo; enter \u0026ldquo;ONPREM\u0026rdquo; (or whatever you like):\nAfterwards you can use the following code:\n#if OnPrem var userMail=GetMailFromUser(fieldUserValue); #else var userMail=FieldUserValue.Email; #endif Final thoughts You are done now. Just keep in mind: If you use any Azure-connections (e.g. for caching, insights, blob-storage) you will have to handle that, too if you need to get rid of any external connection. The custom Compiler Symbols should help you with that task\n","date":"2016-12-13T12:00:00+01:00","image":"/blog/two-apps-at-the-same-time/showBuildConfigurations-300x221_hu_6302578466ea388f.png","permalink":"/blog/two-apps-at-the-same-time/","title":"Two Apps at the same time: Deploy SharePoint Add-In to Cloud AND On-Premise"},{"content":"As you might know: the wayback machine from archive.org saves snapshots of websites. It does this automatically or can be forced to do so. However. If you ever had a 301 redirect on your site you are very much doomed.\nLike browsers also the wayback-machine caches the redirect and does not continue to take snapshots from your sites. Even if you enter your url into the wayback-machine, you still will be redirected.\nYou can however force it to ignore the cache by manually store your site again:\nhttps://web.archive.org/save/[YOURSITE]\nfor example:\nhttps://web.archive.org/save/http://blathering.de\nEt voílà: Everything works nice from now on.\n[Image is CC-BY-NC-ND © David Baldingerhttp://www.dbaldinger.com/opinion_cartoons/second_page/wayback.html]\n","date":"2016-11-15T12:00:00+01:00","permalink":"/blog/wayback-machine-and-301-redirects/","title":"Wayback Machine and 301 redirects"},{"content":"If you work in a company that uses Proxy authentication (aka forces you to enter a password to access websites) and are a developer you know my pain: Some Most applications just don\u0026rsquo;t really like proxies. Visual Studio itself for example does not like proxy authentication at all and responses with a wide variety of error codes (407 for example).\nYou can get rid of all these errors through different workarounds, but these are a real PITA.\nIn addition the application you code itself needs Internet access, for example if you develop Office365 AddIns, you are very much screwed.\nAfter a long research I finally found a solution using Fiddler.\nMost developers already use Fiddler for debugging reasons so there isn\u0026rsquo;t even any software that needs to be installed.\nFirst of all:\nStart your computer. You should not have been entered your Proxy credentials yet. Use Fiddler as proxy in your IE and enter your real Proxy in Fiddler:\nDefault Proxy-Settings to use Fiddler\nProxy-Settings in Fiddler\nDo not select \u0026ldquo;System Proxy\u0026rdquo;. Otherwise. Fiddler will enter itself here, because Fiddler is the System Proxy right now.\nNow open your favorite browser (except FireFox, as it uses its own proxy) and open any site that opens up the Proxy authentication window. Enter your credentials and load that site. In Fiddler you should now see (at least) one entry with response code 407 followed by a few entries with 200:\nSelect the entry with StatusCode 200 and display the Request Headers. Under \u0026ldquo;Security there now is a line with \u0026ldquo;Proxy-Authorization: \u0026ldquo;\u0026hellip;\nCopy that line.\nNow Enter Ctrl-R in Fiddler to open the rules file. Look for\nstatic function OnBeforeRequest(oSession: Session) {\nin that file and add the following line directly at the top of the function:\noSession.oRequest[\u0026ldquo;Proxy-Authorization\u0026rdquo;] = \u0026ldquo;Basic DBDDHKPadasdkahsdasd=\u0026rdquo;;\nwhere you replace the token behind \u0026ldquo;Basic\u0026rdquo; with the value you copied before.\nNow save CustomRules.js and from now on no application has to enter any more Proxy passwords (as long as you use Fiddler as System Proxy)\nIf you are forced to change your Password, remove this line in the configuration file, and repeat the procedure from the beginning with the new password.\nThat\u0026rsquo;s it, mostly. For more comfort I would recommend to use the proxy switcher if you have to change the proxy more often.\n","date":"2016-09-16T12:00:00+02:00","image":"/blog/getting-rid-of-proxy-limitations-using-fiddler/proxyIE_hu_916867fbbfb567d9.png","permalink":"/blog/getting-rid-of-proxy-limitations-using-fiddler/","title":"Getting rid of Proxy limitations using Fiddler"},{"content":"This tutorial assumes that you already installed and configured FHEM and know how to use AngularJS.\nAs you might know I use my Raspberry PI to automate tasks at home. I already detect my SmartPhone, and my FitBit and also control my Webcam and Harmony remote using a raspberry and a web interface.\nNow I also wanted to include actors from HomeMatic thermostats into my configuration and display it in a nice way.\nHardware used:\nHomeMatic Wireless Configuration Adapter LAN (HM-CFG-LAN) HomeMatic Thermostate HM-CC-RT-DN Raspberry Pi V2. Software used:\nHTML/CSS JavaScript AngularJS FHEM PHP (optional) Before we begin make sure you have already installed FHEM and the hardware for it. I will not discuss that here as there are a lot of good tutorials for that. A good (german) tutorial can be found in the FHEM Wiki. (If you got any english documentation, feel free to post it in the comments.)\nI wrote a \u0026ldquo;general\u0026rdquo; PHP script to send commands and receive commands from FHEM. This can also be done without PHP and pure JavaScript-Code. I chose this solution because my configuration website is accessable through the internet, but my FHEM isn\u0026rsquo;t. So JS-Could (running on the client) would not work, but PHP (running on the server) does.\nThe PHP-Script:\n\u0026lt;?php $host=\u0026#34;http://192.168.178.36:8083/fhem?\u0026#34;; $sensor= $\\_GET\\[\u0026#39;sensor\u0026#39;\\]; $param= $\\_GET\\[\u0026#39;param\u0026#39;\\]; $action= $\\_GET\\[\u0026#39;action\u0026#39;\\]; if ($action==\u0026#34;get\u0026#34;) { $get=\u0026#34;detail=SENSOR\u0026amp;dev.getSENSOR=SENSOR\u0026amp;cmd.getSENSOR=get\u0026amp;arg.getSENSOR=param\u0026amp;val.getSENSOR=PARAM\u0026amp;XHR=1\u0026amp;addLinks=1\u0026#34;; } else { $get=\u0026#34;cmd=set%20SENSOR%20desired-temp%20PARAM\u0026amp;XHR=1\u0026#34;; } $replaced=str\\_replace(\u0026#34;SENSOR\u0026#34;,$sensor,$get); $replaced=str\\_replace(\u0026#34;PARAM\u0026#34;,$param,$replaced); $total=$host.$replaced; $ch = curl\\_init(); curl\\_setopt($ch, CURLOPT\\_URL, $total); curl\\_setopt($ch, CURLOPT\\_RETURNTRANSFER, 1); $return = curl\\_exec($ch); echo $return; ?\u0026gt; The only thing you need to modify here is the IP-Adress (or hostname) and maybe the port from the $host - variable. As you can see, this is a simple GET-Request which could easily used through JavaScript (or even just inside a href) also.\nYou can test this script by simply calling it with some parameters:\nhttp://yourserver/fhem.php?action=get\u0026amp;sensor={sensorname}_Clima\u0026amp;param=measured-temp\nReplace \u0026ldquo;Sensorname\u0026rdquo; by the name of your sensor. You should receive the current temperature of the sensor as a result.\nNow it is time to display this data in a fancy way in html. Create any HTML - Page using AngularJS. To use these nice Gauges you need ng-Google-Chart\nNow lets see the HTML-Part containing the AngularJS-Directives to display and set the thermostate values:\n\u0026lt;html\u0026gt;\u0026lt;body ng-app=\u0026#34;piApp\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34; ng-Controller=\u0026#34;heatController\u0026#34; ng-init=\u0026#34;getStats()\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;box\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-lg-12\u0026#34;\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;h2 class=\u0026#34;intro-text text-center\u0026#34;\u0026gt;Heizungs \u0026lt;strong\u0026gt;Einstellungen\u0026lt;/strong\u0026gt; \u0026lt;/h2\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;col-md-12\u0026#34;\u0026gt; \u0026lt;div ng-repeat=\u0026#34;heater in heaterSettings.allHeaters\u0026#34; class=\u0026#34;heater\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;battery\u0026#34;\u0026gt;\u0026lt;i class=\u0026#34;fa fa-4x fa-{{heater.battery.icon}} {{heater.battery.color}}\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div google-chart chart=\u0026#34;heater.gauge\u0026#34; \u0026gt;\u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;caption\u0026#34;\u0026gt;{{heater.name}}\u0026lt;br/\u0026gt; \u0026lt;select ng-model=\u0026#34;heater.ui.wish\u0026#34; ng-change=\u0026#34;setTemperature(heater.id,heater.ui.wish)\u0026#34;\u0026gt; \u0026lt;option ng-repeat=\u0026#34;option in availableTemperatures()\u0026#34; value=\u0026#34;{{option}}\u0026#34;\u0026gt;{{option}}\u0026lt;/option\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt; Some explanation:\npiApp is the AngularJS - App for the website, getStats() loads the current Sensors using FHEM.php mentioned above directly from the start. This html uses Bootstrap (for repsonsive design) and Font Awesome to display the Battery indicator. Both use CSS to achieve this. (The Gauge-Display works without these libraries)\nThe Angular-Controller looks like this:\nangular.module(\u0026#39;piApp\u0026#39;, \\[\u0026#39;googlechart\u0026#39;\\]) .controller(\u0026#39;heatController\u0026#39;, function($scope, $http) { $scope.url=\u0026#39;fhem.php?\u0026#39;; $scope.heaterSettings={ allHeaters:\\[ {name:\u0026#39;Wohnzimmer\u0026#39;, id:\u0026#39;Wohnzimmer\\_Clima\u0026#39;, ui:{},battery:{},gauge:{}}, {name:\u0026#39;Schlafzimmer\u0026#39;,id:\u0026#39;Schlafzimmer\\_Clima\u0026#39;,ui:{},battery:{},gauge:{}}, {name:\u0026#39;Bad\u0026#39;,id:\u0026#39;Bad\\_Clima\u0026#39;,ui:{},battery:{},gauge:{}} \\] }; $scope.getParamUrl=function(sensor,param) { return \u0026#34;action=get\u0026amp;sensor=\u0026#34;+sensor+\u0026#34;\u0026amp;param=\u0026#34;+param; }; $scope.setParamUrl=function(sensor,param) { return \u0026#34;action=set\u0026amp;sensor=\u0026#34;+sensor+\u0026#34;\u0026amp;param=\u0026#34;+param; }; $scope.getBattery=function(sensor, target) { $http.get($scope.url+$scope.getParamUrl(sensor,\u0026#39;batteryLevel\u0026#39;)) .success(function(data) { $scope.setBattery(target,data); }); }; $scope.getStats=function() { $scope.heaterSettings.allHeaters.forEach(function (heater) { // gauge: heater.gauge={}; heater.gauge.type=\u0026#34;Gauge\u0026#34;; heater.gauge.options = { width: 250, height: 250, redFrom: 20, redTo: 30, greenFrom: 0, greenTo: 10, yellowFrom: 10, yellowTo: 20, minorTicks: 10, majorTicks: \\[\u0026#39;0\u0026#39;,\u0026#39;5\u0026#39;,\u0026#39;10\u0026#39;,\u0026#39;15\u0026#39;,\u0026#39;20\u0026#39;,\u0026#39;25\u0026#39;,\u0026#39;30\u0026#39;\\], max:30, animation:{ startup: true, duration: 1000, easing: \u0026#39;inAndOut\u0026#39; } }; // Gemessene Temperatur: $http.get($scope.url+$scope.getParamUrl(heater.id,\u0026#39;measured-temp\u0026#39;)) .success(function(data) { $scope.setData(heater.gauge,data); }); $http.get($scope.url+$scope.getParamUrl(heater.id,\u0026#39;desired-temp\u0026#39;)) .success(function(data) { $scope.setWish(heater.ui,data); }); $scope.getBattery(heater.name,heater); }); }; $scope.setTemperature=function(sensor, temperature) { $http.get($scope.url+$scope.setParamUrl(sensor,temperature)) .success(function(data) { console.log(\u0026#34;Temperatur von \u0026#34;+sensor+\u0026#34; auf \u0026#34;+temperature+\u0026#34; Grad eingestellt\u0026#34;); //$scope.getStats(); }) .error(function(data) { alert(\u0026#34;Fehlgeschlagen\u0026#34;); } ); }; $scope.availableTemperatures=function() { var available=\\[\\]; for(var i=5;i\u0026lt;=30;i=i+0.5) { var iasString= i.toString(); if (Math.round(i)===i) { iasString+=\u0026#39;.0\u0026#39; } available.push(iasString); } available.push(\u0026#34;on\u0026#34;); available.push(\u0026#34;off\u0026#34;); return available; }; $scope.setData=function(target,data) { var value=parseFloat(data.replace(/(\\\\r\\\\n|\\\\n|\\\\r)/gm,\u0026#34;\u0026#34;)); target.data= \\[ \\[\u0026#39;Label\u0026#39;, \u0026#39;Value\u0026#39;\\], \\[\u0026#39;\u0026#39;, value\\]\\]; }; $scope.setWish=function(target,data) { var value=data.replace(/(\\\\r\\\\n|\\\\n|\\\\r)/gm,\u0026#34;\u0026#34;).trim(); target.wish=value; }; $scope.setBattery=function(target,value) { var value=parseFloat(value.replace(/(\\\\r\\\\n|\\\\n|\\\\r)/gm,\u0026#34;\u0026#34;).trim()); var color=\u0026#39;green\u0026#39;; var icon=\u0026#39;battery-full\u0026#39;; if (value\u0026lt;2.1) { color=\u0026#34;red\u0026#34;; icon=\u0026#39;battery-empty\u0026#39;; } else if (value\u0026lt;2.3) { color=\u0026#34;red\u0026#34;; icon=\u0026#39;battery-quarter\u0026#39;; } else if (value\u0026lt;2.6) { color=\u0026#34;yellow\u0026#34;; icon=\u0026#39;battery-half\u0026#39;; } target.battery={ value:value, color:color, icon:icon } }; }) Explanation:\n\u0026ldquo;googlechart\u0026rdquo; in the module-configuration allows to paint those fancy gauges.\nin $scope.heaterSettings.allHeaters you have to modify the Names of your Sensors. Enter as many sensors as you want.\nAll other functions communicate with fhem.php or display values.\n","date":"2016-07-11T12:00:00+02:00","permalink":"/blog/displaying-data-from-homematic-actors-on-a-webinterface/","title":"Displaying Data from HomeMatic Actors on a WebInterface"},{"content":"That remark isn\u0026rsquo;t mine, it is from StackOverflow. :)\nI had an issue that my Office365-App did not work correctly on the machine of ONE user. Sadly though that user was the Microsoft validation crew. I was unable to reproduce the error and did not even see any clue in the Server logs. Everything seemed to work fine.\nFinally I managed to reproduce the error. It only happened under the following conditions: 1. Internet Explorer 11; IE - what else :) 2. Caching in IE has to be on \u0026ldquo;Automatic\u0026rdquo; 3. Developer Console MUST NOT be open 3.1. In fact: Even if you open it and close it afterwards, the error does not appear anymore 4. I had to retract/reinstall the app each time to check this behaviour which took about 10 minutes every time\nI had issues with console on older IEs before, but at least angular uses some magic, so that issue is resolved. Nearly all questions in the web combining IE and console repeat the solution that you may not use \u0026ldquo;console.log()\u0026rdquo;.\nBut the real issue was: Caching. IE - in its wisdom - decided that the API-Call that returns the installation state should be cached. That cached value always responded that the app is not installed, even if it was. So the user kept receiving the \u0026ldquo;please install\u0026rdquo; - Page.\nAny other browser (Chrome, Firefox, \u0026hellip;) doesn\u0026rsquo;t cache these calls. And the IE doesn\u0026rsquo;t either when you open the Developer Console (what you do if you want to debug an application) which makes it really hard to track that error.\nSo the solution is to make sure the call isn\u0026rsquo;t cached using No-Cache-Headers or just by adding a random value to the target address. And of course resist the urge to display a reminder to use another browser. That could be bad for the validation results :)\n","date":"2016-06-14T12:00:00+02:00","permalink":"/blog/its-because-they-at-microsoft-hate-web-developers/","title":"It's because they at Microsoft hate web developers"},{"content":" ⚠️ Warning Development stopped! Please read this before downloading FAD\n📝 Note Keep your Version updated! Always click onto the \u0026ldquo;Check for Updates\u0026rdquo; in the Program after installation\nThe FlashAirDownloader 2 is out there (Because of my Laziness I will just call it FAD from now on)\nA lot of people used the FlashAirDownloader which was meant as a personal tool and quick hack to access my Toshiba FlashAir Card. As more and more people asked me for features (and some for the code) I decided to make it open source so more folks can help improve the tool.\nSadly so: The code was a mess. As I told you: Just a quick hack. It would be to embarrassing to make this public :D So I decided to rewrite it from scratch: This time making it right regarding code quality and UI design. Nonetheless this program still uses Framework 4.0, cause I know there are Windows XP users out there who have no other possibilities to access the FlashAir.\nBut enough of talk. Here is the new FAD:\nYou can download the source directly from Github. Feel free to make the code better. Or download the binary here:\n(The download from the old site is gone, please build from source.)\nYou will get an annoying amount of warnings from your browser and/or operating system. This is because the file isn\u0026rsquo;t signed. Valid signing requires a valid certificate which does not come for free. (and requires some amount of work) Feel free to build from source in GitHub if you have any doubts.\nFirst start After Installation and starting FAD you should see something like this:\nIf your FlashAir - card is already configured it should be found and the display should change to this:\nFor now: Keep calm and do not click on any of the blue squares. Use \u0026ldquo;File/Settings\u0026rdquo; instead.\nSetup Program Settings Check if your settings are ok. Here is a small description from the settings:\nFlashAir Url This is the Url from your FlashAir. You can try if you can access your FlashAir by entering that address into your Browser Local Path Here the Images will be saved when using the Auto-Mode Existing Files (Auto mode) Decide what to do if the file already exists. Delete files after downloading (Auto mode) If checked, the files will be deleted after a successful download from the FlashAir Flashair Imagepath Where your Images are located (may contain subfolders) Filetypes to copy (Auto mode) Decide if you only want image files or other files, too Load Thumbnails If enabled Thumbnails from Images on the FlashAir will be collected and shown. Might take a while. Thumbnails for local files are always displayed regardless of this setting. Automatic folder Creation (Auto Mode) * Multiple Cards Use your Card ID or AppID to create subfolders if you use more than one card * Create By Date Decide in what format the directories should be named * Custom You can als create your own custom path. {0} stands for the CardId, {1} for the AppId and {2} for the current date. Examples about the date-format can be found here Styling Decide if (and what) images should be displayed on the start page and the settings page of FAD. Don\u0026rsquo;t forget to click onto [Save] under Card Settings in the lower left area of the screen.\nCard Settings Now after the application is set up it is time to check your FlashAir-Card. Just put your SD-Card into the computer and click onto [Load] in the upper left corner. After a few seconds three new tabs should appear: \u0026ldquo;Vendor\u0026rdquo;, \u0026ldquo;WLANSD\u0026rdquo; and \u0026ldquo;Disable Functions\u0026rdquo;\nClick onto \u0026ldquo;Vendor\u0026rdquo; to setup the most important settings. I will only describe the most critical values. A complete list is available at the Toshiba FlashAir Website.\nFirst make sure the Connection Timeout is correct. It must be at least 1 Minute. You can also set it to \u0026ldquo;0\u0026rdquo; to disable timeout completely. \u0026ldquo;App-Info\u0026rdquo; and \u0026ldquo;Card ID\u0026rdquo; can be modified if you want to use those values in the automatic folder creation.\nIf your Network settings already work, don\u0026rsquo;t touch them here. Otherwise follow the instructions on the FlashAir website.\nNow make sure the \u0026ldquo;Upload Directory\u0026rdquo; fits the \u0026ldquo;Flashair Imagepath\u0026rdquo; in the \u0026ldquo;Application Settings\u0026rdquo;.\nThe last setting to check is \u0026ldquo;Upload Enabled\u0026rdquo;. This is required if you want to be able to save files onto the Flashair or want to delete files after downloading.\nNow all you have to do is to click [Save] in the upper Left region and FAD is ready to go.\nFor the first run I would advice just keeping your card in your card-reader. It works exactly there like in your camera, but you can modify your settings more easy this way.\nAuto - Mode After FAD found your card click onto \u0026ldquo;Auto Mode\u0026rdquo; to start automatic downloading of your data:\nAt first the files are loaded recursively from your card. In the next step (if activated) the thumbnails gets downloaded. Then the files are being downloaded and deleted (if selected).\nIf the thumbnails cannot be retrieved red squares are displayed instead.\nFile Explorer The file explorer does not download automatically images from the Flashair. Instead you can browse through your card and copy files manually. Just mark them by clicking and select the [\u0026laquo;] or [\u0026raquo;] - Button to copy them#\nKnown issues https://www.youtube.com/watch?v=PtXtIivRRKQ\nThe FlashAir is sometimes a bit unstable (at least mine). There are a lot of reasons why something goes wrong. That\u0026rsquo;s why FAD tries up to five times to download or upload data. (Just Thumbnails are only tried once). There are many reasons why a download does not work. There can be a timeout, the Card can \u0026ldquo;crash\u0026rdquo; (it is a webserver running on it) or the camera just goes into standby. So often the best solution is to try again and or restart the camera and re-enter the FlashAir.\nAny more questions? Just look at the documentation on GitHub or open an issue there if you encounter any problems.\nLove it?\nFeel free to send some money to my Panama-Account\n","date":"2016-05-01T12:00:00+02:00","image":"/blog/flashairdownloader-2/autocopy_hu_dd06e496e28f5a46.png","permalink":"/blog/flashairdownloader-2/","title":"FlashAirDownloader 2"},{"content":"After detecting my phone using WiFi (which does only work under Marshmallow while the phone is loading or the display is enabled) and detecting the fitbit device I came back to my original task to detect my phone using Bluetooth.\nWhile there are many documents out there that describe this on different Linux and even raspberry variants none of those really worked for me. The raspberry 2 (Debian Jessie) seem to be a bit different there.\nFirst of all you need a Bluetooth USB dongle. I buyed the Logilink BT00015 (available at amazon.com, amazon.de or elsewhere). I would expect that others work, too. But as always: On Linux, drivers can be tricky and it\u0026rsquo;s always good to know working hardware.\nBefore installing drivers it is always a good idea to update everything:\nsudo apt-get update sudo apt-get upgrade Then install the libraries needed for bluetooth\nsudo apt-get install \u0026ndash;no-install-recommends bluetooth\nI received an error that some dependencies could not be resolved. I fixed that by entering the following commands:\nsudo dpkg --configure -a sudo apt-get install -f `Now insert the USB dongle and restart the raspberry` sudo shutdown -r now To check if the USB-dongle is detected, enter the following command:\nlsusb\nThis lists all USB devices. There should be one offering Bluetooth - services\nNow lets check if the Bluetooth itself works:\n/etc/init.d/bluetooth status The response should now contain the word \u0026ldquo;Running\u0026rdquo;:\npi@raspberrypi:~ $ /etc/init.d/bluetooth status ● bluetooth.service - Bluetooth service Loaded: loaded (/lib/systemd/system/bluetooth.service; enabled) Active: active (running) since Sat 2016-02-27 19:31:45 UTC; 17min ago Docs: man:bluetoothd(8) Main PID: 7841 (bluetoothd) Status: \u0026#34;Running\u0026#34; CGroup: /system.slice/bluetooth.service └─7841 /usr/lib/bluetooth/bluetoothd `Now it\u0026#39;s time to check for devices. Do nothing with your phone right now and enter:` hcitool scan `This should display all Bluetooth-devices in Range. As you haven\u0026#39;t done anything with your phone yet it should not be displayed there. The output should be something like this:` Scanning ... 7C:2F:BE:EF:FA:CE Ole 00:04:BE:EF:FA:CE SHIELD B8:86:BE:EF:FA:CE Fette Glotze 88:53:BE:EF:FA:CE TOMATO-PC 48:44:BE:EF:FA:CE TVBluetooth (I modified the MAC addresses. I am not absolutely sure if anyone could do evil things using the MACs, but I prefer not to find that out :) )\nNow put your phone into discovery mode. On Android Marshmallow you just have to enter your Bluetooth-settings for this. Then re-scan your devices. There should be one more, now:\nhcitool scan 7C:2F:BE:EF:FA:CE Ole 00:04:BE:EF:FA:CE SHIELD B8:86:BE:EF:FA:CE Fette Glotze 88:53:BE:EF:FA:CE TOMATO-PC 48:44:BE:EF:FA:CE TVBluetooth F8:95:C7:H0:0H:0H G4 Now just note your Mac-Address. We will not pair our device. That takes a few steps and additional packages and additional problems. Pairing is not needed once we know the mac-address.\nNow we cam just check the presence by using the info command. Simply leave your Bluetooth-settings on the phone and enter sudo hcitool info F8:95:C7:H0:0H:0H Event though your phone isn\u0026rsquo;t in discovery-mode anymore you will get some status information like the device name:\nRequesting information ... BD Address: F8:95:C7:H0:0H:0H Device Name: G4 LMP Version: 4.1 (0x7) LMP Subversion: 0x6109 Manufacturer: Broadcom Corporation (15) `if you turn off Bluetooth on your phone and re-enter that command you will receive a different response:` Requesting information ... Can\u0026#39;t create connection: Input/output error `So that\u0026#39;s it. Just use the good old grep in your bash-script to check whether the phone is there or not:` if hcitool info F8:95:C7:H0:0H:0H | grep -q \u0026#39;Device Name\u0026#39;; then // Do whatever you like if you found it fi Again: Have fun :)\n","date":"2016-02-27T12:00:00+01:00","permalink":"/blog/detecting-smartphone-via-bluetooth-on-linux-raspberry-pi-2/","title":"Detecting Smartphone via Bluetooth on Linux (Raspberry Pi 2)"},{"content":"As I wrote before I track the presence of my smartphone using WiFi. Since Android Marshmellow this doesn\u0026rsquo;t work anymore, because when switching to idle mode, the WiFi is disconnected. The easiest way to get around this issue would be to use Bluetooth instead. And in fact: I already decided to go that way. But then my company gave me a nice present: A \u0026ldquo;Fitbit Charge HR\u0026rdquo;. While my company assured, that the HR is not for Human Resource and does not try to track if I am programming enough I decided to give this a try.\nBeing a nerd I am not that much into that \u0026ldquo;fitness\u0026rdquo;-area. But I like the device as a small watch and the vibration alarm is nice too. I will surely sync this with my Appointments lateron. Getting some information about how many steps it takes to get the beer out of the cellar or the chips out of the kitchen is a nice benefit. Not really useful. But at least nerdy :)\nSo I plan to wear this device for a while. So it should be a good idea to use it for tracking my presence. Maybe in combination with a phone-detection as a fall-back. First I tried to use a standard USB-dongle to detect it. But as this device works on Bluetooth Low Energy (BLE) it cannot be detected with normal Bluetooth-Dongles. So hcitool scan or even hcitool lescan did not receive any results. Luckily the Fitbit ships with an USB-dongle. And even better, there is a package called \u0026ldquo;galileo\u0026rdquo; which purpose is to sync the fitbit. Installation is easy:\nsudo apt-get install python-usb python-requests sudo pip install galileo Now install the dongle and restart the raspberry.\nIf you enter the following command sudo galileo -v you should see something like\n2016-02-27 16:28:21,875:INFO: 1 trackers discovered All you have to do now to check if the fitbit is in range automaticly is some bash-magic:\nif galileo -v | grep -q \u0026#39;1 trackers\u0026#39;; then // Do whatever you like if you found it fi ","date":"2016-02-27T12:00:00+01:00","permalink":"/blog/tracking-fitbit-presence-under-linux-raspberry-pi-2/","title":"Tracking fitbit presence under Linux (Raspberry Pi 2)"},{"content":"So my surveillance cam now only records when I am not at home. But what if I forget to turn off the TV? That should happen automaticly, too! But fear not: There is a solution - at least if you own a harmony hub. With the help of the \u0026ldquo;HarmonyHubControl\u0026rdquo; - tool (written in c++) this is done quite easily.\nFirst of all you need to put the source onto the Raspberry. Don\u0026rsquo;t be afraid. No rocket science here. All needed tools should be already on your Linux distribution. The easiest way would be to clone the source via git: git clone git://git.code.sf.net/p/harmonyhubcontrol/code harmonyhubcontrol-code If you prefer you may - of course - also use the long way and download those files in a zip file.\nNow after you have the source code on your Pi you have to compile it. Done with ease. Just a single\nmake and you created the file \u0026ldquo;HarmonyHubControl\u0026rdquo; in that directory. Now all you need to know is the IP-address from your hub. You usually can find it in your WiFi-Router under the name \u0026ldquo;HarmonyHub\u0026rdquo;\nLet\u0026rsquo;s try if the connection works:\n./HarmonyHubControl \\[Login\\] \\[Password\\] \\[Ip\\] Warning! Double-check your password. If you enter a wrong one, you can still stert the activities, but on the next login in your \u0026ldquo;MyHarmony\u0026rdquo;-application you will be thrown out for a while. Don\u0026rsquo;t ask why this works this way. Better ask Logitech instead.\nThe Login is your email-address. If username and password are right you should see the following result: So let\u0026rsquo;s list all activities: ./HarmonyHubControl \\[Login\\] \\[Password\\] \\[Ip\\] list\\_activies As you can see there is an activity with the id \u0026ldquo;-1\u0026rdquo;. This activity shuts down all devices. For testing let\u0026rsquo;s just start an activity: ./HarmonyHubControl \\[Login\\] \\[Password\\] \\[Ip\\] start\\_activity 15231836 does start the FireTV in my case, ./HarmonyHubControl \\[Login\\] \\[Password\\] \\[Ip\\] start\\_activity -1 does shut em all down.\nIf you want to, do some fancy stuff with that possibilities. Use your stereo as an alarm clock? Have fun! (your neighbours, too, btw) :)\n","date":"2016-01-15T12:00:00+01:00","image":"/blog/control-your-harmony-hub-with-a-raspberry-pi-linux/hh1_hu_8bb48e2bca1c02c6.png","permalink":"/blog/control-your-harmony-hub-with-a-raspberry-pi-linux/","title":"Control your Harmony Hub with a Raspberry Pi (Linux)"},{"content":"I already wrote a script on my Raspberry that starts and stops the surveillance mode of my camera using the \u0026ldquo;Surveillance Station\u0026rdquo; on the Synology DiskStation. Now I wanted to extend that tool and light the LEDs of the cam when it is recording and turning them off if it isn\u0026rsquo;t.\nThis has two reasons: First: I do not want to use the cam for \u0026ldquo;secret recordings\u0026rdquo;, damaging the private spheres of my visitors. Second: I can easily see if the cam is working as expected. Now my cam has the option to activate the LEDs or deactivate from a web interface, but it is all-or-nothing. It does not depend if it is recording or not. Which isn\u0026rsquo;t a surprise as the camera does not trigger the recordings itself but this is done by the Surveillance Station.\nI own (and use) the \u0026ldquo;Robocam 4\u0026rdquo;, sold by 7Links. But this hardware is quite common and used under different names, too.\nFinding out the right commands is quite straight-forward: Open the webinterface and listen what happens there using the developer console in Chrome:\nThe \u0026ldquo;Request URL\u0026rdquo; and \u0026ldquo;Form Data\u0026rdquo; are the most interesting informations here. Before entering that stuff into the Linux-shell let\u0026rsquo;s test this with a more \u0026ldquo;comfortable\u0026rdquo; interface. The chrome extension \u0026ldquo;Postman\u0026rdquo; offers a nice way to play around with POST-Requests. The setting should be \u0026ldquo;x-www-form-urlencoded\u0026rdquo; here:\nIf everything works as desired it is time to put that command to Linux using shell commands. Especially easy if the come does not only accept POST-requests, but GET-requests, too.\nhttp://192.168.0.88/admin/system.cgi?led=2\nworked like a charm in my case.\nThe only thing that\u0026rsquo;s left is the authentifivcation. If you receive a classic login-screen like from a .htaccess password-protection it can be easily added to the url. So wget http://username:password@192.168.0.88/admin/system.cgi?led=2 turns on the LEDs wget http://username:password@192.168.0.88/admin/system.cgi?led=0 turns them off\n(0=off, 1=on, 2=blinking)\nJust add these lines to your existing script and the LEDs should be work just like needed :)\n","date":"2016-01-15T12:00:00+01:00","image":"/blog/switching-the-led-of-a-surveillance-cam-on-and-off-using-linux/konsole_hu_e820623820643dd.png","permalink":"/blog/switching-the-led-of-a-surveillance-cam-on-and-off-using-linux/","title":"Switching the LED of a surveillance cam on and off using Linux"},{"content":"I own a Synology diskstation which has the \u0026ldquo;surveillance station\u0026rdquo; on board which records content from any LAN (or WiFi)-enabled webcam and sends an alarm when someone is moving at my home. It is possible to enter time schedules when the camera should be enabled and when it shouldn\u0026rsquo;t. But that does not really work the way I wanted. When I come home earlier or even have a day off I have to disable the camera manually.\nSo I decided that I want to use my smartphone to trigger the surveillance mode. Like most of us I always carry the phone with me. So it should be easy: If the phone is at home, I am, too. So the camera should be off. The same way when I am not at home.\nThe solution for this was surprisingly simple: A small bash-script first checks if the phone is in the network. The Surveillance station cam gets enabled or disabled according to this one condition.\nBut first things first. First of all, you should go to your routers admin interface and make sure, your phone will always have the same IP in your local WiFi. This works different on any reouter. On TP-Link it is called \u0026ldquo;reserve Ip\u0026rdquo;:\nNow all you need is a Linux machine. I took a fresh Raspberry for this goal. Any simple distribution should be fine. We won\u0026rsquo;t need any special libraries.\nAfter you connected to your Raspberry (or other Linux) create the following shell script via vi (or any other tool you prefer):\nuser=\u0026#39;pi\u0026#39; pass=\u0026#39;circle\u0026#39; server=\u0026#39;sauger:5000\u0026#39; phone=\u0026#39;192.168.0.7\u0026#39; dt=$(date \u0026#39;+%d/%m/%Y %H:%M:%S\u0026#39;); cd $(dirname $0) touch lgstate.txt read oldValue \u0026lt; lgstate.txt if ping -c 3 ${phone} | grep \u0026#39;bytes from\u0026#39;; then if \\[ $oldValue != \u0026#39;isda\u0026#39; \\]; then echo \\[${dt}\\] \u0026#39;stop recording\u0026#39; echo \u0026#39;isda\u0026#39; \u0026gt;lgstate.txt #login wget -q --keep-session-cookies --save-cookies cookies.txt -O- \u0026#34;http://${server}/webapi/auth.cgi?api=SYNO.API.Auth\u0026amp;method=Login\u0026amp;version=3\u0026amp;account=${user}\u0026amp;passwd=${pass}\u0026#34; #disable wget -q --load-cookies cookies.txt -O- \u0026#34;http://${server}/webapi/entry.cgi?api=SYNO.SurveillanceStation.Camera\u0026amp;method=Disable\u0026amp;version=3\u0026amp;cameraIds=1\u0026#34; \u0026gt;dev\u0026gt;null #logout wget -q --load-cookies cookies.txt -O- \u0026#34;http://${server}/webapi/auth.cgi?api=SYNO.API.Auth\u0026amp;method=Logout\u0026amp;version=3\u0026#34; \u0026gt;dev\u0026gt;null else echo \\[${dt}\\] \u0026#39;still there...\u0026#39; fi else if \\[ $oldValue != \u0026#39;iswech\u0026#39; \\]; then echo \\[${dt}\\] \u0026#39;start recording\u0026#39; echo \u0026#39;iswech\u0026#39; \u0026gt;lgstate.txt #login wget -q --keep-session-cookies --save-cookies cookies.txt -O- \u0026#34;http://${server}/webapi/auth.cgi?api=SYNO.API.Auth\u0026amp;method=Login\u0026amp;version=3\u0026amp;account=${user}\u0026amp;passwd=${pass}\u0026#34; #enable wget -q --load-cookies cookies.txt -O- \u0026#34;http://${server}/webapi/entry.cgi?api=SYNO.SurveillanceStation.Camera\u0026amp;method=Enable\u0026amp;version=3\u0026amp;cameraIds=1\u0026#34; \u0026gt;dev\u0026gt;null #logout wget -q --load-cookies cookies.txt -O- \u0026#34;http://${server}/webapi/auth.cgi?api=SYNO.API.Auth\u0026amp;method=Logout\u0026amp;version=3\u0026#34; \u0026gt;dev\u0026gt;null else echo \\[${dt}\\] \u0026#39;still noone at home\u0026#39; fi fi Change \u0026ldquo;user\u0026rdquo; and \u0026ldquo;pass\u0026rdquo; in the beginning of the script with your user credentials. You can create a new user for this or use the \u0026ldquo;admin\u0026rdquo; - user. The user has to have admin rights sadly so don\u0026rsquo;t use this script on any machine that can be accessed by anyone but you.\n\u0026ldquo;server\u0026rdquo; is your server address enter the hostname (or ip) and port there\n\u0026ldquo;phone\u0026rdquo; is your phone\u0026rsquo;s IP-address\nSo what does this script do?\nFirst it tries to ping your smartphone. If this is successful, it checks if this is the first time that works. If it isn\u0026rsquo;t the first time, nothing has to be done. But if the phone wasn\u0026rsquo;t accessable before it logs onto the synology and disables the camera using the web api.\nThe same way it enables the camera if the phone cannot be contacted anymore.\nThe script does only minimum outputs. If you run into problems, just remove the \u0026ldquo;\u0026gt;dev\u0026gt;null\u0026rdquo; part on any line to get more information.\nBefore you can start the script you should create an empty file where the state is saved by entereing touch lgstate.txt You can run that script by entering \u0026ldquo;./pingPhone.sh\u0026rdquo; on the command line. You can watch the changes live in the Surveillance Station -View of the Synology. Changes are displayed instantly. You do not need to refresh the page.\nIf you have more than one camera change the value of \u0026ldquo;cameraIds\u0026rdquo; to another value that is needed. You also can select multiple cameras by using commas: \u0026ldquo;\u0026amp;cameraIds=1,4,5\u0026rdquo;\nFinally if you want to run this script automaticly you can create a simple cronjob for it\nopen crontab -e and enter the following line: \\*/15 \\* \\* \\* \\* /home/pi/pingPhone.sh \u0026gt;\u0026gt;/home/pi/pingPhone.log (of course the path may differ on your system)\nNow the presence will be checked every 15 minutes and the cameras will enabled or disabled accordingly. Now all you have to do is to modify your time schedule in the surveillance station that the camera is always in detection mode.\n","date":"2016-01-07T12:00:00+01:00","image":"/blog/automate-your-synology-surveillancestation-with-some-simple-linux-commands/reserveip_hu_ec0476895da7cc0d.png","permalink":"/blog/automate-your-synology-surveillancestation-with-some-simple-linux-commands/","title":"Automate your Synology Surveillancestation with some simple Linux - commands"},{"content":"For a lot of teams, GIT is the first choice for maintaining the code. So it is for mine. But this post isn\u0026rsquo;t limited to GIT and should work with SVN or TFS with little modifications, too.\nWhy is the Git flow important when using SCRUM? Aren\u0026rsquo;t those just two worlds, anyway?\nWell. Kind of. But while SCRUM is quite flexible, scrums paradigm still has some very basic contents when it comes to manage projects:\nYou and your team work in sprints. Inside these sprints there are user-stories you want to implement. We decided to map sprints and user stories to our git flow.\nClassic git flow: You usually have the following structure in a git flow: (More fancy description and image at Atlassian)\nYou always got your master branch. From that branch you create single branches for each Hotfix. Your daily work is done in feature branches that start at the development branch. When a feature is finished, you merge it into your development branch. For a release, the development branch (which contains the added features) is the root of a new Release-branch. That release is tested and maybe modified until it fits the needs of your customer. When you are happy with that release you merge it back into the development branch as well as the master branch.\n\u0026ldquo;Scrumified\u0026rdquo; Git Flow: When trying to put SCRUM into the flow you keep your basic structure but add two additional layers to it. The single features are not created directly from the development branch. Instead at first for each sprint a new branch is created from the development branch. From the sprint there are branches created from each user story. After you\u0026rsquo;ve done that, the features are created from the corresponding stories. Merging back goes the same way. You merge your features back into the user story and the user story back into the sprint before merging the sprint back into your development branch.\nThis is a bit more work (and might look a little bit chaotic on these pictures at first) but it offers the advantages you have from \u0026ldquo;normal\u0026rdquo; git flow also to your sprint. Like before you only merge features that are finished. But now you also only merge user stories that are complete. And as a small bonus you can always analyze which user story was started and finished in which sprint. A Win-Win-situation like those unlikable marketing guys would call it. (If they were able to understand a single word I just wrote here) :)\n","date":"2015-03-04T12:00:00+01:00","image":"/blog/scrum-and-git-optimized-gitflow-with-a-lot-of-fancy-colors/gitflow1_hu_ef8620f4237a401d.png","permalink":"/blog/scrum-and-git-optimized-gitflow-with-a-lot-of-fancy-colors/","title":"SCRUM and Git: Optimized Gitflow // with a lot of fancy colors :)"},{"content":"Everyone knows spam. Everyone hates spam. And everyone who has his email-address written on the net (for example in the NIC-records for a domain) gets a lot of spam.\nI am one of those guys who gets a lot of spam. Of course there are anti-spam solutions out there, but the best would be to not even receive spam. And spam filters need a lot of work and / or do not filter 100% perfect. That is why I decided to use a public email that has to go through a \u0026ldquo;gatekeeper\u0026rdquo;. A PHP-script that tells everyone that this is not my real mail address, but that there is another one where I could be contacted.\nThat email-address is not displayed in plain text but as an image to get rid of all those bots. SOME bots however should be able to get through: For example my hoster who needs to validate my email periodically. So these messages just are being forwarded.\nI wanted to be as simple as possible. That\u0026rsquo;s why this script does not need much more than plain php. Only a replacement for phps \u0026ldquo;mail\u0026rdquo; was needed.\nFirst step: Download swiftmailer. Just put the contents of the \u0026ldquo;lib\u0026rdquo; - directory onto your web space. That\u0026rsquo;s it. Ready to go.\nSimple include the following line to access swiftmailer:\nrequire\\_once './swift/lib/swift\\_required.php'; To access your mail you need to enter the IMAP-Settings. POP would work, too, but IMAP would be much more useful because we do not want to download the email-body of the messages processed:\n// IMAP-Settings: $hostname = \u0026#34;{imap.hostname.com:993/imap/ssl}INBOX\u0026#34;; $username = \u0026#34;username\u0026#34;; $password = \u0026#34;password\u0026#34;; `Now enter your email-addresses. $targetMail is your real (\u0026#34;hidden\u0026#34;) email-address where valid emails should be forwarded to. $thisMail is the public address that receives the spam.` $targetMail=\u0026#34;private@mydomain.com\u0026#34;; $thisMail=\u0026#34;webmaster@mydomain.com\u0026#34;; Next you need to create an image with your favourite picture editing tool (GiMP, Paint.NET, PaintShopPro, etc.) This image should only contain your private email. Take a font you like. You might also use a \u0026ldquo;Captcha\u0026rdquo;-font. I - personally - hate that\u0026hellip; :)\nUpload that image to your web server and enter the url of that image. PNG would be a good choice.\n$image=\u0026ldquo;http://www.example.com/images/mail.png\";\nNext, enter the text the sender should receive. By default everyone receives $templateHtml. For those who do not have the capabilities to display html (or don\u0026rsquo;t want to) a simple text-mail ($templatePlain) will be displayed. Users with a html-browser will see your email-image directly in the mail. Plain-text mail-clienst will display a link to the image instead.\n$templateForward defines the mail content YOU will see if a mail is forwarded to you from a valid sender\nSo it is time to be creative :)\n$templateHtml=\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;Thanks for your mail. Please note that this mail is no more. It has ceased to be! It is expired and gone to meet its maker! This is an Ex-Mail!\u0026lt;br/\u0026gt;But don\u0026#39;t be afraid, there is a replacement for that. Just send your mail to: \u0026lt;br/\u0026gt;\u0026lt;img src=\\\\\u0026#34;\\_\\_EMBED\\_\\_\\\\\u0026#34;/\u0026gt;\u0026lt;br/\u0026gt;\u0026lt;br/\u0026gt;If you cannot see that image: Click onto the following link to see my: \u0026lt;a href=\\\\\u0026#34;\\_\\_URL\\_\\_\\\\\u0026#34;\u0026gt;new mail-address\u0026lt;/a\u0026gt;\u0026lt;br/\u0026gt;And no: This mail will not be forwarded. No one will see this until you forward it to my new mail-address. Thanks!\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;; $templatePlain=\u0026#34;Thanks for your mail. Please note that this mail is no more. It has ceased to be! It is expired and gone to meet its maker! This is an Ex-Mail!\\\\r\\\\nBut don\u0026#39;t be afraid, there is a replacement for that. Just click onto the following link to see my: new mail-address: \\_\\_URL\\_\\_\u0026#34;; $templateForward=\u0026#34;new message redirected\u0026#34;; As you can see there are two special tags in those templates. \u0026ldquo;__EMBED__\u0026rdquo; will be replaced with the embedded image (html-template only) and \u0026ldquo;__URL__\u0026rdquo; will contain a link to that image.\nThe last variable you can define is the white-list. Everything entered there is interpreted as a valid sender. Enter multiple values seperated by commas. $whitelist = \u0026quot;uberspace,denic.de,domainbox.net,romrobot.com,inwx.de,millerntor.hamburg,dotnet.work\u0026quot;; that\u0026rsquo;s all for configuration. Now just some explanation what the script does:\nFirst of all the script checks your IMAP-Server for unread messages. It collects the header of each of those messages and compares the sender with the white-list. If the sender is recognized, the mail body is downloaded and sent as an attachment to the mail address you defined as $targetMail.\nIf the sender is not in the white-list, an automatic reply is generated using your html-template. It contains the image you downloaded as an attachment and embedded image of the mail. The last step is to mark the mail as read so it won\u0026rsquo;t be processed again.\nIn a perfect world\u0026hellip;\nEverything works now. But you can (and will) get problems sending the mail to the sender if the sender\u0026rsquo;s address does not comply with RFC 2822. While most spammers DO comply, mom DOESN\u0026rsquo;T\u0026hellip; :) That\u0026rsquo;s because there is a mail client from a tiny little software company in Redmond, where they do not know much about standards. But this is no big problem at all. Just open lib\\classes\\swift\\Mime\\Headers\\MailboxHeader.php from swiftmailer and put /* */ around the throw-part of the last function:\nprivate function \\_assertValidAddress($address) { if (!preg\\_match(\u0026#39;/^\u0026#39;.$this-\u0026gt;getGrammar()-\u0026gt;getDefinition(\u0026#39;addr-spec\u0026#39;).\u0026#39;$/D\u0026#39;, $address)) { /\\* throw new Swift\\_RfcComplianceException(\u0026#39;Address in mailbox given \\[\u0026#39;.$address.\u0026#39;\\] does not comply with RFC 2822, 3.6.2.\u0026#39;);\\*/ } } `So finally here is the complete script.` \u0026lt;?php require\\_once \u0026#39;./swift/lib/swift\\_required.php\u0026#39;;// IMAP-Settings: $hostname = \u0026#34;{imap.example.com:993/imap/ssl}INBOX\u0026#34;; $username = \u0026#34;username\u0026#34;; $password = \u0026#34;password\u0026#34;; // Script-Settings: $targetMail=\u0026#34;private@example.com\u0026#34;; // \u0026#34;hidden\u0026#34; target mail-address $thisMail=\u0026#34;webmaster@example.com\u0026#34;; // \u0026#34;official\u0026#34; mail-address (which receives the spam) $image=\u0026#34;http://www.dotnet.work/reply/mail.png\u0026#34;; // Image to be shown $templateHtml=\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;Thanks for your mail. Please note that this mail is no more. It has ceased to be! It is expired and gone to meet its maker! This is an Ex-Mail!\u0026lt;br/\u0026gt;But don\u0026#39;t be afraid, there is a replacement for that. Just send your mail to: \u0026lt;br/\u0026gt;\u0026lt;img src=\\\\\u0026#34;\\_\\_EMBED\\_\\_\\\\\u0026#34;/\u0026gt;\u0026lt;br/\u0026gt;\u0026lt;br/\u0026gt;If you cannot see that image: Click onto the following link to see my: \u0026lt;a href=\\\\\u0026#34;\\_\\_URL\\_\\_\\\\\u0026#34;\u0026gt;new mail-address\u0026lt;/a\u0026gt;\u0026lt;br/\u0026gt;And no: This mail will not be forwarded. No one will see this until you forward it to my new mail-address. Thanks!\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;; $templatePlain=\u0026#34;Thanks for your mail. Please note that this mail is no more. It has ceased to be! It is expired and gone to meet its maker! This is an Ex-Mail!\\\\r\\\\nBut don\u0026#39;t be afraid, there is a replacement for that. Just click onto the following link to see my: new mail-address: \\_\\_URL\\_\\_\u0026#34;; $templateForward=\u0026#34;new message redirected\u0026#34;; $whitelist = \u0026#34;uberspace,denic.de,domainbox.net,romrobot.com,inwx.de,millerntor.hamburg,dotnet.work\u0026#34;; $allowedSenders=explode(\u0026#34;,\u0026#34;,$whitelist); $inbox = imap\\_open($hostname,$username,$password) or die(\u0026#34;Sorry, connection failed\u0026#34;); $emails = imap\\_search($inbox,\u0026#34;UNSEEN\u0026#34;); $count=0; echo \u0026#34;\u0026lt;pre\u0026gt;\u0026#34;; if($emails) { $transport = Swift\\_MailTransport::newInstance(); foreach($emails as $email) { $count++; $header = imap\\_fetch\\_overview($inbox,$email,0); $subject=$header\\[0\\]-\u0026gt;subject; $sender=$header\\[0\\]-\u0026gt;from; $messageHeader = imap\\_fetchheader($inbox, 1) if (strpos($messageHeader,\u0026#34;X-RESPONDERDATE\u0026#34;)!==false) { continue; // no wild bouncing, please } if (strpos($sender,$targetMail!==false || strpos($sender,$thisMail!==false) { // We want no loops... :) continue; } echo \u0026#34;retrieving \u0026#34;.$subject.\u0026#34; from \u0026#34;.$sender.\u0026#34;\\\\r\\\\n\u0026#34;; // Whitelist: $isWhiteListed=false; foreach($allowedSenders as $allowedSender) { if (stripos($sender,$allowedSender)!==false) { $isWhiteListed=true; break; } } if ($isWhiteListed) { $email\\_body = imap\\_body($inbox, $email); $mailer = Swift\\_Mailer::newInstance($transport); $message = Swift\\_Message::newInstance() -\u0026gt;setSubject(\u0026#34;Fw:\u0026#34;.$subject) -\u0026gt;setFrom($sender) -\u0026gt;setTo($targetMail) -\u0026gt;setBody($templateForward) ; $attachment = Swift\\_Attachment::newInstance($email\\_body, \u0026#39;origin.eml\u0026#39;, \u0026#39;message/rfc822\u0026#39;); $message-\u0026gt;attach($attachment); $result = $mailer-\u0026gt;send($message); echo \u0026#34;whitelist redirect\\\\r\\\\n\u0026#34;; } else { $mailer = Swift\\_Mailer::newInstance($transport); $message = Swift\\_Message::newInstance(); $swiftheaders = $message-\u0026gt;getHeaders(); $swiftheaders-\u0026gt;addTextHeader(\u0026#39;X-RESPONDERDATE\u0026#39;, date(\u0026#39;Y-m-d H:i:s\u0026#39;)); $cid = $message-\u0026gt;embed(Swift\\_Image::fromPath($image)); $html=str\\_replace(\u0026#34;\\_\\_URL\\_\\_\u0026#34;,$image,$templateHtml); $html=str\\_replace(\u0026#34;\\_\\_EMBED\\_\\_\u0026#34;,$cid,$html); $plain=str\\_replace(\u0026#34;\\_\\_URL\\_\\_\u0026#34;,$image,$templatePlain); $message -\u0026gt;setSubject(\u0026#34;Re:\u0026#34;.$subject) -\u0026gt;setFrom($thisMail) -\u0026gt;setTo($sender) -\u0026gt;setBody($html, \u0026#39;text/html\u0026#39;) -\u0026gt;addPart($plain, \u0026#39;text/plain\u0026#39;) ; $result = $mailer-\u0026gt;send($message); echo \u0026#34;answered\\\\r\\\\n\u0026#34;; } imap\\_setflag\\_full($inbox, imap\\_uid($inbox,$email), \u0026#34;\\\\\\\\Seen \\\\\\\\Flagged\u0026#34;, ST\\_UID); } } imap\\_close($inbox); echo \u0026#34;done redirecting \u0026#34;.$count.\u0026#34; mails\\\\r\\\\n\u0026#34;; echo \u0026#34;\u0026lt;/pre\u0026gt;\u0026#34;; ?\u0026gt; `Just save this program as \u0026#34;reply.php\u0026#34;, send test-mails and open \u0026#34;reply.php\u0026#34; in your browser to test the settings. When everything works fine you can simply add a cronjob so this script is run automaticly. The following example runs the script every 5 minutes:` \\*/5 \\* \\* \\* \\* php /var/www/reply.php Just use the comments if you need any help\n","date":"2015-02-13T12:00:00+01:00","permalink":"/blog/imap-html-php-autoresponder-autoreply/","title":"IMAP-HTML-Autoresponder with PHP"},{"content":" ⚠️ Warning Development stopped! Please read this before downloading FAD\n📝 Note **New Version available!**This version is obsolete. I would strongly suggest to use the FAD2 instead. You can download it here: http://dotnet.work/fad2.\nToshibas FlashAir is an SD-Card with built-in WiFi. It allows you to download Pictures from your DigiCam through WiFi. Sadly there is no working Windows-Application.\nThe FlashAir-Downloader solves that issue. It allowes Bulk-Downloading of your images. Your pictures can be saved in folders created automaticly containing date-information.\nInstallation:\nUnpack the Zip and start the Setup.Exe\nStart the Application and go to Settings:\nAdjust the Settings if needed. Especially for “local Path”.\nTo use the Delete-Option you have to adjust the configuration of the SD-Card. Insert the SD-Card into your computer and open the file “CONFIG” file in the hidden “SD_WLAN”-folder with a text editor. Add the line UPLOAD=1 to the CONFIG-File.\nThats it. Close that window, turn on your camera and click onto “CONNECT”. If the connection works, you should see the folders and images of your SD-Card. Enter a folder or preview an image by double-clicking onto the icon. Mark the images you want to download and start downloading by clicking onto “Download”.\nI give no guarantee about anything. This application shouldn’t do anything bad. But if your Camera explodes: Don’t blame me.This tool is and always will be free. But I don’t mind donations, of course :)\n— Update V 1.1 —\nI just updated the FlashAirDownloader to Version 1.1.\nThe following has changed:\nBugfix: If the file date is quite old (\u0026lt;2000) it could not be downloaded. This has been fixed. [*Yeah! Year 2000 bug in 2014. Well done, Toshiba *🙄 😁 ] New Feature: You can now change the contents of the CONFIG – File directly. Just insert the SD-Card into your computer and Select “Card Properties New Feature: If you use more than just One FlashAir-Card you can now automatically create subfolders for each card. You can use the internal “CID” ID for that or the Application ID which may contain any custom name. (Tested with Windows 7 and Windows 8.1)\nA new Version is available: http://dotnet.work/fad2\n","date":"2014-09-25T12:00:00+02:00","permalink":"/blog/flashair-downloader-for-windows/","title":"FlashAir Downloader for Windows"},{"content":"I already offered a solution to empty a SharePoint list at light speed\nBut this solution hast two big disadvantages:\nYou cannot empty a list that has more entries thatn the allowed list view threshold (default is 5000) All Lookups will be lost This solution has non of those drawbacks. It is much slower than that solution but also much faster than “normal” deletion of SPListItems or using a Caml-Query to do that.\nJust be asure that this is not thread-safe. So make sure noone is writing into that list in the same time or that lines might not be deleted.\nprivate int rest; public static void TruncateList(Guid listId, int limit) { SPSecurity.RunWithElevatedPrivileges(delegate() { using (SPSite site = new SPSite(SPContext.Current.Site.ID)) { using (SPWeb web = SPContext.Current.Web) { try { \\_rest = limit; web.AllowUnsafeUpdates = true; SPList listCopy = web.Lists\\[listId\\]; ContentIterator iterator = new ContentIterator(); iterator.ProcessListItems(listCopy, ProcessItem, ProcessError); web.Update(); web.AllowUnsafeUpdates = false; } catch (Exception ex) { if (!ex.Message.Contains(\u0026#34;Limit reached\u0026#34;)) { Debug.WriteLine(ex); } } } } } ); } public static bool ProcessError(SPListItem item, Exception ex) { if (!ex.Message.Contains(\u0026#34;Limit reached\u0026#34;)) { Debug.WriteLine(ex); } return true; } public static void ProcessItem(SPListItem item) { if (rest\u0026lt;=0) { Debug.WriteLine(\u0026#34;Limit reached\u0026#34;); throw new Exception(\u0026#34;Limit reached\u0026#34;); return; } item.Delete(); \\_rest–; } ","date":"2012-08-10T12:00:00+02:00","permalink":"/blog/emptying-sharepoint-list-the-quite-fast-way/","title":"Emptying SharePoint-List the (quite) fast way"},{"content":"SharePoint is damn slow when deleting lines from lists. Each entry can take up to some seconds which isn’t nice when you have to delete thousands of lines.\nAlso no Trunc-Methode is available.\nOther solutions found on the net creating xml-files to bulk delete aren’t very fast either.\nThat’s why I decided to write my own fast truncate-method for SharePoint-Lists.\nAll content of that list will be deleted. Use at your own risk!\nWhat it does:\nFirst a template of that list is being created. After that the list is deleted completely. Than a new list using that template is being created and at the end that template is being deleted.\nWorks damn fast (just a few seconds even on big lists). IF anything goes wrong you can use the template to create a list manually.\npublic void TruncateList(Guid listID) { SPSecurity.RunWithElevatedPrivileges(delegate() { using (SPSite site = new SPSite(SPContext.Current.Site.ID)) { using (SPWeb web = SPContext.Current.Web) { try { web.AllowUnsafeUpdates = true; // alte Werte merken SPList listCopy = web.Lists\\[listID\\]; string title = listCopy.Title; string description = listCopy.Description; string backupName = string.Format(\u0026#34;BACKUP{0}{1:d} {1:HH}{1:mm}\u0026#34;, title,DateTime.Now); // save template listCopy.SaveAsTemplate(backupName, backupName, string.Empty, false); SPListTemplate template = web.Site.GetCustomListTemplates(web)\\[backupName\\]; // delete list listCopy.Delete(); web.Lists.Add(title, description, template); // remove template SPList gallery = web.Lists\\[\u0026#34;List Template Gallery\u0026#34;\\]; foreach (SPListItem item in gallery.Items.Cast().Where(item =\u0026gt; item.Title == backupName)) { item.Delete(); break; } web.Update(); web.AllowUnsafeUpdates = false; } catch (Exception ex) { \\_log.Error(ex); throw; } } } } ); } ","date":"2012-08-09T12:00:00+02:00","permalink":"/blog/emptying-sharepoint-lists-the-ultra-fast-way/","title":"Emptying SharePoint lists the ultra fast way"}]