Homelab, Linux, JS & ABAP (~˘▾˘)~
 

[CAP] Dynamically set destination in package.json for an external connection

  "cds": {
    "requires": {
      "sfsf": {
        "kind": "odata-v2",
        "credentials": {
          "destination": "<set during runtime>",
          "path": "/odata/v2",
          "requestTimeout": 18000000
        }
      }
    }
  },
  /*
   * Handover query to some external SF OData Service to fecth the requested data
   */  
  srv.on("READ", Whatever, async req => {

        const sf_api_def = cds.env.requires['sfsf'] //defined in package.json

        sf_api_def.credentials.destination = "myDestinationName" //set your Destination name, could come from a customizing table

        const sfsfSrv = await cds.connect.to(sf_api_def)

        return await sfsfSrv.run(req.query)
    })

[CAP] Providing destinations locally using .env

https://sap.github.io/cloud-sdk/docs/js/features/connectivity/destinations#local-environment-variable

The provided example

destinations="[{\"name\": \"TESTINATION\", \"url\": \"http://url.hana.ondemand.com\", \"username\": \"DUMMY_USER\", \"password\": \"EXAMPLE_PASSWORD\"}]"

didn’t work for me. I had to change it to the following format:

destinations=[{"name": "TESTINATION", "url": "http://url.hana.ondemand.com", "username": "DUMMY_USER", "password": "EXAMPLE_PASSWORD"}]

You can check the current value using:

process.env.destinations

[ABAP] Create ToC for a given transport, release it and download it as ZIP

*&---------------------------------------------------------------------*
*& Report Z_ZIP_TOC
*&---------------------------------------------------------------------*
*& For the givin transport, this report creates a transport of copies (ToC),
*& releases it, and then download the ToC as ZIP file to your filesystem.
*& The given request can be in sate unreleased!
*&---------------------------------------------------------------------*
REPORT Z_ZIP_TOC.

INITIALIZATION.

  SELECTION-SCREEN BEGIN OF BLOCK bl01 WITH FRAME TITLE TEXT-t01.
  PARAMETERS p_trkorr LIKE e070-trkorr OBLIGATORY.
  PARAMETERS p_ttext  TYPE as4text.
  SELECTION-SCREEN END OF BLOCK bl01.

  SELECTION-SCREEN BEGIN OF BLOCK bl02 WITH FRAME TITLE TEXT-t02.
  PARAMETERS p_sapdir TYPE string LOWER CASE OBLIGATORY DEFAULT '/usr/sap/trans/'.
  PARAMETERS p_lcldir TYPE string LOWER CASE OBLIGATORY DEFAULT 'C:\temp\'.
  SELECTION-SCREEN END OF BLOCK bl02.

AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_trkorr.
  CALL FUNCTION 'TR_F4_REQUESTS'
    EXPORTING
      iv_trkorr_pattern   = p_trkorr
    IMPORTING
      ev_selected_request = p_trkorr.

START-OF-SELECTION.

  " Read description of provided transport
  DATA ls_request TYPE trwbo_request.
  CALL FUNCTION 'TR_READ_REQUEST'
    EXPORTING
      iv_read_e07t     = 'X'
      iv_trkorr        = p_trkorr
    CHANGING
      cs_request       = ls_request
    EXCEPTIONS
      error_occured    = 1
      no_authorization = 2
      OTHERS           = 3.
  IF sy-subrc <> 0.
    MESSAGE 'Could not read Transport description' TYPE 'E'.
  ENDIF.


  " Read all objects for provided transport
  DATA: lt_objects TYPE  tr_objects,
        lt_keys    TYPE  tr_keys.
  CALL FUNCTION 'TR_GET_OBJECTS_OF_REQ_AN_TASKS'
    EXPORTING
      is_request_header      = VALUE trwbo_request_header( trkorr = p_trkorr )
      iv_condense_objectlist = 'X'
    IMPORTING
      et_objects             = lt_objects
      et_keys                = lt_keys
    EXCEPTIONS
      invalid_input          = 1
      OTHERS                 = 2.
  IF sy-subrc <> 0.
    MESSAGE 'Could not read Transport objects' TYPE 'E'.
  ENDIF.


  " Create new ToC
  DATA ls_request_header TYPE trwbo_request_header.
  CALL FUNCTION 'TR_INSERT_REQUEST_WITH_TASKS'
    EXPORTING
      iv_type           = 'T'
      iv_text           = COND as4text( WHEN p_ttext IS INITIAL THEN ls_request-h-as4text
                                                                ELSE p_ttext )
      iv_owner          = sy-uname
      iv_target         = 'DUM'
    IMPORTING
      es_request_header = ls_request_header
    EXCEPTIONS
      insert_failed     = 1
      enqueue_failed    = 2
      OTHERS            = 3.
  IF sy-subrc <> 0.
    MESSAGE 'Transport creation failed' TYPE 'E'.
  ENDIF.
  DATA(lv_trkorr_toc) = ls_request_header-trkorr.


  " Add all object to the new ToC
  CALL FUNCTION 'TRINT_APPEND_COMM'
    EXPORTING
      wi_exclusive       = 'X'
      wi_sel_e071        = 'X'
      wi_sel_e071k       = 'X'
      wi_trkorr          = lv_trkorr_toc
    TABLES
      wt_e071            = lt_objects
      wt_e071k           = lt_keys
    EXCEPTIONS
      e071k_append_error = 1
      e071_append_error  = 2
      trkorr_empty       = 3
      OTHERS             = 4.
  IF sy-subrc <> 0.
    MESSAGE 'Could not append objects to ToC' TYPE 'E'.
  ENDIF.


  " Release ToC Transport
  DATA lt_messages TYPE ctsgerrmsgs.
  CALL FUNCTION 'TRINT_RELEASE_REQUEST'
    EXPORTING
      iv_trkorr                   = lv_trkorr_toc
*     IV_DIALOG                   = 'X'
*     IV_AS_BACKGROUND_JOB        = ' '
*     IV_SUCCESS_MESSAGE          = 'X'
*     IV_WITHOUT_OBJECTS_CHECK    = ' '
*     IV_CALLED_BY_ADT            = ' '
*     IV_CALLED_BY_PERFORCE       = ' '
*     IV_WITHOUT_DOCU             = ' '
      iv_without_locking          = 'X'
*     IV_DISPLAY_EXPORT_LOG       = 'X'
*     IV_IGNORE_WARNINGS          = ' '
*     IV_SIMULATION               = ' '
    IMPORTING
      et_messages                 = lt_messages
    EXCEPTIONS
      cts_initialization_failure  = 1
      enqueue_failed              = 2
      no_authorization            = 3
      invalid_request             = 4
      request_already_released    = 5
      repeat_too_early            = 6
      object_lock_error           = 7
      object_check_error          = 8
      docu_missing                = 9
      db_access_error             = 10
      action_aborted_by_user      = 11
      export_failed               = 12
      execute_objects_check       = 13
      release_in_bg_mode          = 14
      release_in_bg_mode_w_objchk = 15
      error_in_export_methods     = 16
      object_lang_error           = 17
      OTHERS                      = 18.
  IF sy-subrc <> 0.
    MESSAGE 'Could not release ToC' TYPE 'E'.
    cl_demo_output=>display( lt_messages ).
  ENDIF.


  " Download released ToC as ZIP
  DATA lv_xcontent_k TYPE xstring.
  DATA lv_xcontent_r TYPE xstring.

  DATA(lv_transdir_k) = |{ p_sapdir }cofiles/K{ lv_trkorr_toc+4 }.{ lv_trkorr_toc(3) }|.
  DATA(lv_transdir_r) = |{ p_sapdir }data/R{    lv_trkorr_toc+4 }.{ lv_trkorr_toc(3) }|.


  TRY.

      " K
      OPEN  DATASET lv_transdir_k FOR INPUT IN BINARY MODE.
      READ  DATASET lv_transdir_k INTO lv_xcontent_k.
      CLOSE DATASET lv_transdir_k.

      " R
      OPEN  DATASET lv_transdir_r FOR INPUT IN BINARY MODE.
      READ  DATASET lv_transdir_r INTO lv_xcontent_r.
      CLOSE DATASET lv_transdir_r.


      " Add to ZIP
      DATA(lo_zipper) = NEW cl_abap_zip( ).

      lo_zipper->add( name    = |K{ lv_trkorr_toc+4 }.{ lv_trkorr_toc(3) }|
                      content = lv_xcontent_k ).

      lo_zipper->add( name    = |R{ lv_trkorr_toc+4 }.{ lv_trkorr_toc(3) }|
                      content = lv_xcontent_r ).


      " Download ZIP
      DATA(lv_xzip) = lo_zipper->save( ).

      " Convert to raw data
      DATA(lt_data) = cl_bcs_convert=>xstring_to_solix( iv_xstring = lv_xzip ).

      " Set zip filename
      DATA(lv_zip_name) = COND #( WHEN p_ttext IS INITIAL THEN |{ ls_request-h-as4text }_{ lv_trkorr_toc }|
                                                          ELSE |{ p_ttext              }_{ lv_trkorr_toc }| ).

      " Replace every character that is not [a-zA-Z0-9_] with '_'.
      REPLACE ALL OCCURRENCES OF REGEX '[^\w]+' IN lv_zip_name WITH '_'.

      cl_gui_frontend_services=>gui_download( EXPORTING filename = p_lcldir && lv_zip_name && '.zip'
                                                        filetype = 'BIN'
                                              CHANGING  data_tab = lt_data ).

    CATCH cx_root INTO DATA(e_text).
      MESSAGE e_text->get_text( ) TYPE 'E'.
  ENDTRY.

  MESSAGE |{ lv_zip_name }.zip created and downloaded to { p_lcldir }| TYPE 'S'.

[CAP] Use Destinations from Subscriber Account

While working on this topic, there are currently two chapters in the CAP docs regarding this topic:

And some SCN posts: herehere and here

But all this didn’t help me to complete the task. Got the solution which finally worked for me from this SAP Sample Susaas (two snippets: here and here).

server.js

const cds = require("@sap/cds")

cds.on('served', async () => {
    const { 'cds.xt.SaasProvisioningService': provisioning } = cds.services
    // Add provisioning logic only if multitenancy is there..
    if (provisioning) {
        let tenantProvisioning = require('./provisioning')
        provisioning.prepend(tenantProvisioning)
    } else {
        console.log(">>> There is no service, therefore does not serve multitenancy!")
    }
})

module.exports = cds.server

provisioning.js

const xsenv = require('@sap/xsenv')
xsenv.loadEnv()

module.exports = (service) => {

    service.on('dependencies', async (req, next) => {
        let dependencies = await next()
        const services = xsenv.getServices({
            registry: { tag: 'SaaS' },
            destination: { tag: 'destination' }
        })
        dependencies.push({ xsappname: services.destination.xsappname }) //adds the subscriber destination as dependency
        console.log(">>> SaaS Dependencies:", JSON.stringify(dependencies))
        return dependencies
    })

}

It injects the destination dependency by manually reading it using xsenv package and returning it in a dependencies callback handler.

[BTP] Create sap-successfactors-extensibility service providing a technicalUser

Prerequisite, you have registered an SAP SuccessFactors system in your Global Account (see here). Creating the sap-successfactors-extensibility service can be done via command line:

#Created the service instance
#An HTTP destination on a subaccount level with the same name as the service instance name is automatically generated
cf create-service sap-successfactors-extensibility api-access myInstanceName -c '{"systemName": "SFCPART000000","technicalUser": "sfadmin"}'

#Bind the instance to an application
cf bind-service myApp-srv myInstanceName 

Find an explanation of the parameters here: https://help.sap.com/docs/btp/sap-business-technology-platform/authentication-type-json-file

This service instance will result in creating:

  • a separate OAuth2 client application on SFSF side (can find in SF in Manage OAuth2 Client Applications)
  • a separate destination definition on a BTP sub-account level

The technicalUser parameter can be specified only during creation. There is no possibility to provide it afterwards using cf update-service. It may be possible to manually update the technicalUser in the destination, which got automatically created. But I did not test this yet.

Of course, the same service creation can also be done via mta.yaml.

resources:
 #####################################################################################################################
  # SuccessFactors Extensibility Service
  #####################################################################################################################
  - name: myInstanceName 
    type: org.cloudfoundry.managed-service
    #type: org.cloudfoundry.existing-service
    parameters:
      service: sap-successfactors-extensibility
      service-plan: api-access
      config:
        systemName: SFCPART000000 # <-- Provide your system name
        technicalUser: sfadmin

For initial deployment, you need the line type: org.cloudfoundry.managed-service. For all further deployments, you have to comment that line out and comment in the next line type: org.cloudfoundry.existing-service. Else you will receive an error. Read more about that behavior here:https://github.com/SAP-samples/successfactors-extension-calculate-employee-seniority/issues/2

[Home Assistant] Midea Comfee MSAF5-12HRDN8-QE R32

Vor kurzem haben wir uns eine Klimaanlage zugelegt. Diese nutzen wir zum Heizen und Kühlen unseres Büros. Dabei handelt es sich um dieses Gerät der Marke Midea (in DE vor allem als Comfee vermarktet).

Der schlimmste Teil der Installation war wie so häufig nicht die Montage, sondern die Einrichtung des Innengerätes über die App. Um das Gerät in das lokale Wifi zu bringen, muss man sich mit einem Access Point des Gerätes verbinden und dann in der App eine Einrichtung durchlaufen. Nach mehreren gescheiterten versuchen mit einem Android Telefon, habe ich dann zu einem iPhone gegriffen. Hiermit klappte die Einrichtung erheblich besser! Obwohl das Gerät dann im lokalen Netz erreichbar ist, erfolgt die Steuerung dann über eine Cloud… mit entsprechend langsamen Reaktionszeiten. Meiner Meinung nach quasi unbenutzbar.

Um das Gerät ohne großes manuelles Zutun betrieben zu können (also ohne App oder Fernbindung), habe ich daher nach einer entsprechenden Home Assistant Integration gesucht.


Also erstes stieß ich auf diese: https://github.com/nbogojevic/homeassistant-midea-air-appliances-lan

So ganz habe ich dabei nicht verstanden, ob die Integration nun lokal oder via der Cloud steuert. Zumindest musste man beim Einrichtung seine Cloud Zugangsdaten angeben und darüber wird dann das lokale Gerät ermittelt. Beim Testen fiel mir auf, dass sobald man diese Integration nutzte, das Gerät nicht mehr über die App steuerbar war (und umgekehrt). Der Grund ist, dass man den gleichen Zugang nicht auf zwei verschiedenen Geräten nutzen kann. Nach einer Einrichtung eines zweiten Accounts trat das Problem nicht mehr auf und man konnte beides parallel nutzen.

So richtig zuverlässig funktionierte das Ein- oder Ausschalten des Gerätes bei mir jedoch nicht. Ggf. weil die Befehle auch über die Cloud liefen und nicht lokal!?

Auch die Entities der Integration schienen für die Klimaanlage nicht vollständig zu sein. Insgesamt schien die Integration eher für anderen Midea Gerätetypen geeignet zu sein, als Klimaanlagen.


Darauf hin habe ich mir diese Integration angeschaut und bin aktuell auch bei dieser geblieben: https://github.com/georgezhao2010/midea_ac_lan

Die Integration bietet die Möglichkeit, selber die entsprechen Sensoren und Controls zu aktivieren. Man muss also durch Trial-and-Error herausfinden, welche von dem eigenen Gerät unterstützt bzw. publiziert werden

oder in den Developer Tools nach der zugehörigen Climate Entity suchen und sich die Werte anschauen.

Nach etwas herumprobieren, bin ich bei dieser Auswahl geblieben.

Leider funktioniert die Realtime Power nicht, aber immerhin der Gesamtverbrauch. Ich habe daher noch eine Zigbee Steckdose mit Strommessung davor geschaltet, um den Verbrauch in Echtzeit zu überwachen. Außerdem konnte ich die Indoor und Outdoor Temperature Werte noch nicht wirklich nachvollziehen. Sie verändern sich m.M.n. recht merkwürdig. In der vorherigen Integration wurden hier irgendwie genauere Werte angezeigt. Jemand hat dazu auch bereits ein Issue aufgemacht.


Eine entsprechende Thermostat Card wird auch direkt auf dem Dashboard erzeugt.

Eine etwas schönere Anzeige bekommt man mit der Simple Thermostat Card:

type: custom:simple-thermostat
entity: climate.146235046568457_climate
step_size: 1
sensors:
  - entity: sensor.lumi_lumi_weather_temperature
    name: Büro
header: {}
layout:
  mode:
    icons: true
    names: true


Der wirklich interessante Teil ist dann natürlich erst durch Automationen zu verwirklichen. Ich lasse die Klimaanlage z.B. automatisiert bei entsprechenden Uhrzeiten und Temperaturen einschalten. Oder auch, wenn z.B. grade ausreichend Strom von dem Balkonkraftwerk erzeugt wird. Und natürlich wird sie automatisch ausgeschaltet, wenn alle das Haus verlassen.

Insgesamt funktioniert die Integration nun seit einigen Wochen sehr zuverlässig.

YouTube ad-free consumption and other helpful addons

When I was at a friend’s house the other day and a YouTube playlist was playing on the TV, I was really shocked. After every video, there were one or two ads (and some of them even in between). I was not aware how extremely many ads you get when using YouTube without an ad blocker. Therefore, a small collection of apps, add-ons and links that help to make YouTube a bit more enjoyable:

Browser

AndroidTV / FireTV

Android

HTPC

DNS Blocking (i.e. with PiHole)

Update 15.01.2024: When using YouTube in a Browser in combination with uBlock, you likely receive the following message right now: “Ad blockers are not allowed on YouTube”. The only working solutions to prevent this message and to continue watching ad -free is disabling uBlock on YouTube and use this Script with Tampermonkey (at least for me).